Parcourir la source

Merge branch 'rm-microservice' into next

Michael Bromley il y a 4 ans
Parent
commit
dac1aac3b2
99 fichiers modifiés avec 786 ajouts et 2265 suppressions
  1. 61 111
      packages/admin-ui-plugin/src/plugin.ts
  2. 11 41
      packages/asset-server-plugin/src/plugin.ts
  3. 1 9
      packages/asset-server-plugin/src/types.ts
  4. 1 0
      packages/core/e2e/collection.e2e-spec.ts
  5. 6 1
      packages/core/e2e/default-search-plugin.e2e-spec.ts
  6. 3 3
      packages/core/e2e/fixtures/test-plugins/list-query-plugin.ts
  7. 2 59
      packages/core/e2e/fixtures/test-plugins/with-all-lifecycle-hooks.ts
  8. 0 35
      packages/core/e2e/fixtures/test-plugins/with-config-and-bootstrap.ts
  9. 22 0
      packages/core/e2e/fixtures/test-plugins/with-config.ts
  10. 2 2
      packages/core/e2e/fixtures/test-plugins/with-job-queue.ts
  11. 0 46
      packages/core/e2e/fixtures/test-plugins/with-worker-controller.ts
  12. 0 3
      packages/core/e2e/job-queue.e2e-spec.ts
  13. 1 1
      packages/core/e2e/order-item-price-calculation-strategy.e2e-spec.ts
  14. 5 79
      packages/core/e2e/plugin.e2e-spec.ts
  15. 1 1
      packages/core/e2e/shop-catalog.e2e-spec.ts
  16. 1 1
      packages/core/src/api/common/request-context.ts
  17. 1 9
      packages/core/src/app.module.ts
  18. 0 0
      packages/core/src/async/async-observable.ts
  19. 1 0
      packages/core/src/async/index.ts
  20. 8 115
      packages/core/src/bootstrap.ts
  21. 9 0
      packages/core/src/cache/cache.module.ts
  22. 1 0
      packages/core/src/cache/index.ts
  23. 38 0
      packages/core/src/cache/request-context-cache.service.spec.ts
  24. 28 0
      packages/core/src/cache/request-context-cache.service.ts
  25. 7 7
      packages/core/src/cli/populate.ts
  26. 3 8
      packages/core/src/common/types/injectable-strategy.ts
  27. 20 20
      packages/core/src/config/catalog/default-product-variant-price-calculation-strategy.spec.ts
  28. 4 3
      packages/core/src/config/catalog/default-product-variant-price-calculation-strategy.ts
  29. 1 1
      packages/core/src/config/catalog/product-variant-price-calculation-strategy.ts
  30. 5 25
      packages/core/src/config/config.module.ts
  31. 0 1
      packages/core/src/config/config.service.mock.ts
  32. 0 9
      packages/core/src/config/config.service.ts
  33. 0 7
      packages/core/src/config/default-config.ts
  34. 1 1
      packages/core/src/config/job-queue/job-queue-strategy.ts
  35. 0 60
      packages/core/src/config/vendure-config.ts
  36. 1 2
      packages/core/src/index.ts
  37. 1 0
      packages/core/src/job-queue/constants.ts
  38. 15 19
      packages/core/src/job-queue/job-queue.service.spec.ts
  39. 24 27
      packages/core/src/job-queue/job-queue.service.ts
  40. 3 13
      packages/core/src/job-queue/job-queue.ts
  41. 2 2
      packages/core/src/job-queue/polling-job-queue-strategy.ts
  42. 1 4
      packages/core/src/plugin/default-search-plugin/default-search-plugin.ts
  43. 61 106
      packages/core/src/plugin/default-search-plugin/indexer/indexer.controller.ts
  44. 40 69
      packages/core/src/plugin/default-search-plugin/indexer/search-index.service.ts
  45. 0 41
      packages/core/src/plugin/default-search-plugin/types.ts
  46. 3 4
      packages/core/src/plugin/plugin-common.module.ts
  47. 1 13
      packages/core/src/plugin/plugin-metadata.ts
  48. 2 93
      packages/core/src/plugin/plugin.module.ts
  49. 0 85
      packages/core/src/plugin/vendure-plugin.ts
  50. 0 2
      packages/core/src/process-context/index.ts
  51. 0 22
      packages/core/src/process-context/process-context.module.ts
  52. 0 30
      packages/core/src/process-context/process-context.ts
  53. 0 117
      packages/core/src/service/controllers/collection.controller.ts
  54. 0 22
      packages/core/src/service/controllers/tax-rate.controller.ts
  55. 0 2
      packages/core/src/service/helpers/order-calculator/order-calculator.spec.ts
  56. 10 7
      packages/core/src/service/helpers/order-calculator/order-calculator.ts
  57. 1 1
      packages/core/src/service/helpers/order-modifier/order-modifier.ts
  58. 0 3
      packages/core/src/service/initializer.service.ts
  59. 2 39
      packages/core/src/service/service.module.ts
  60. 75 38
      packages/core/src/service/services/collection.service.ts
  61. 1 1
      packages/core/src/service/services/order-testing.service.ts
  62. 3 3
      packages/core/src/service/services/order.service.ts
  63. 37 27
      packages/core/src/service/services/product-variant.service.ts
  64. 15 21
      packages/core/src/service/services/tax-rate.service.ts
  65. 0 21
      packages/core/src/service/types/collection-messages.ts
  66. 0 10
      packages/core/src/service/types/tax-rate-messages.ts
  67. 1 1
      packages/core/src/testing/order-test-utils.ts
  68. 0 1
      packages/core/src/worker/constants.ts
  69. 0 3
      packages/core/src/worker/index.ts
  70. 0 25
      packages/core/src/worker/message-interceptor.ts
  71. 0 44
      packages/core/src/worker/types.ts
  72. 0 37
      packages/core/src/worker/worker-monitor.ts
  73. 0 27
      packages/core/src/worker/worker-service.module.ts
  74. 0 41
      packages/core/src/worker/worker.module.ts
  75. 0 62
      packages/core/src/worker/worker.service.ts
  76. 5 1
      packages/create/templates/index-worker.hbs
  77. 5 3
      packages/create/templates/vendure-config.hbs
  78. 8 7
      packages/dev-server/dev-config.ts
  79. 10 6
      packages/dev-server/index-worker.ts
  80. 12 6
      packages/dev-server/index.ts
  81. 0 3
      packages/dev-server/populate-dev-server.ts
  82. 7 25
      packages/dev-server/test-plugins/google-auth/google-auth-plugin.ts
  83. 6 39
      packages/dev-server/test-plugins/keycloak-auth/keycloak-auth-plugin.ts
  84. 0 5
      packages/elasticsearch-plugin/e2e/elasticsearch-plugin-uuid.e2e-spec.ts
  85. 0 5
      packages/elasticsearch-plugin/e2e/elasticsearch-plugin.e2e-spec.ts
  86. 42 69
      packages/elasticsearch-plugin/src/elasticsearch-index.service.ts
  87. 98 151
      packages/elasticsearch-plugin/src/indexer.controller.ts
  88. 1 2
      packages/elasticsearch-plugin/src/plugin.ts
  89. 1 41
      packages/elasticsearch-plugin/src/types.ts
  90. 4 8
      packages/email-plugin/src/dev-mailbox.ts
  91. 0 29
      packages/email-plugin/src/email-processor.controller.ts
  92. 4 2
      packages/email-plugin/src/email-processor.ts
  93. 0 2
      packages/email-plugin/src/plugin.spec.ts
  94. 18 52
      packages/email-plugin/src/plugin.ts
  95. 3 10
      packages/email-plugin/src/types.ts
  96. 4 1
      packages/pub-sub-plugin/src/pub-sub-job-queue-strategy.ts
  97. 0 7
      packages/testing/src/config/test-config.ts
  98. 7 7
      packages/testing/src/data-population/populate-for-testing.ts
  99. 8 41
      packages/testing/src/test-server.ts

+ 61 - 111
packages/admin-ui-plugin/src/plugin.ts

@@ -1,3 +1,4 @@
+import { MiddlewareConsumer, NestModule, OnApplicationBootstrap, OnModuleDestroy } from '@nestjs/common';
 import { DEFAULT_AUTH_TOKEN_HEADER_KEY } from '@vendure/common/lib/shared-constants';
 import { DEFAULT_AUTH_TOKEN_HEADER_KEY } from '@vendure/common/lib/shared-constants';
 import {
 import {
     AdminUiAppConfig,
     AdminUiAppConfig,
@@ -5,20 +6,9 @@ import {
     AdminUiConfig,
     AdminUiConfig,
     Type,
     Type,
 } from '@vendure/common/lib/shared-types';
 } from '@vendure/common/lib/shared-types';
-import {
-    ConfigService,
-    createProxyHandler,
-    LanguageCode,
-    Logger,
-    OnVendureBootstrap,
-    OnVendureClose,
-    PluginCommonModule,
-    RuntimeVendureConfig,
-    VendurePlugin,
-} from '@vendure/core';
+import { ConfigService, createProxyHandler, Logger, PluginCommonModule, VendurePlugin } from '@vendure/core';
 import express from 'express';
 import express from 'express';
 import fs from 'fs-extra';
 import fs from 'fs-extra';
-import { Server } from 'http';
 import path from 'path';
 import path from 'path';
 
 
 import { defaultAvailableLanguages, defaultLanguage, DEFAULT_APP_PATH, loggerCtx } from './constants';
 import { defaultAvailableLanguages, defaultLanguage, DEFAULT_APP_PATH, loggerCtx } from './constants';
@@ -30,6 +20,11 @@ import { defaultAvailableLanguages, defaultLanguage, DEFAULT_APP_PATH, loggerCtx
  * @docsCategory AdminUiPlugin
  * @docsCategory AdminUiPlugin
  */
  */
 export interface AdminUiPluginOptions {
 export interface AdminUiPluginOptions {
+    /**
+     * @description
+     * The route to the admin ui.
+     */
+    route: string;
     /**
     /**
      * @description
      * @description
      * The port on which the server will listen. If not
      * The port on which the server will listen. If not
@@ -49,26 +44,6 @@ export interface AdminUiPluginOptions {
      * version, e.g. one pre-compiled with one or more ui extensions.
      * version, e.g. one pre-compiled with one or more ui extensions.
      */
      */
     app?: AdminUiAppConfig | AdminUiAppDevModeConfig;
     app?: AdminUiAppConfig | AdminUiAppDevModeConfig;
-    /**
-     * @description
-     * The hostname of the Vendure server which the admin ui will be making API calls
-     * to. If set to "auto", the admin ui app will determine the hostname from the
-     * current location (i.e. `window.location.hostname`).
-     *
-     * @deprecated Use the adminUiConfig property instead
-     * @default 'auto'
-     */
-    apiHost?: string | 'auto';
-    /**
-     * @description
-     * The port of the Vendure server which the admin ui will be making API calls
-     * to. If set to "auto", the admin ui app will determine the port from the
-     * current location (i.e. `window.location.port`).
-     *
-     * @deprecated Use the adminUiConfig property instead
-     * @default 'auto'
-     */
-    apiPort?: number | 'auto';
     /**
     /**
      * @description
      * @description
      * Allows the contents of the `vendure-ui-config.json` file to be set, e.g.
      * Allows the contents of the `vendure-ui-config.json` file to be set, e.g.
@@ -109,11 +84,9 @@ export interface AdminUiPluginOptions {
 @VendurePlugin({
 @VendurePlugin({
     imports: [PluginCommonModule],
     imports: [PluginCommonModule],
     providers: [],
     providers: [],
-    configuration: config => AdminUiPlugin.configure(config),
 })
 })
-export class AdminUiPlugin implements OnVendureBootstrap, OnVendureClose {
+export class AdminUiPlugin implements NestModule {
     private static options: AdminUiPluginOptions;
     private static options: AdminUiPluginOptions;
-    private server: Server;
 
 
     constructor(private configService: ConfigService) {}
     constructor(private configService: ConfigService) {}
 
 
@@ -126,56 +99,8 @@ export class AdminUiPlugin implements OnVendureBootstrap, OnVendureClose {
         return AdminUiPlugin;
         return AdminUiPlugin;
     }
     }
 
 
-    /** @internal */
-    static async configure(config: RuntimeVendureConfig): Promise<RuntimeVendureConfig> {
-        const route = 'admin';
-        const { app } = this.options;
-        const appWatchMode = this.isDevModeApp(app);
-        let port: number;
-        if (this.isDevModeApp(app)) {
-            port = app.port;
-        } else {
-            port = this.options.port;
-        }
-        config.apiOptions.middleware.push({
-            handler: createProxyHandler({
-                hostname: this.options.hostname,
-                port,
-                route: 'admin',
-                label: 'Admin UI',
-                basePath: appWatchMode ? 'admin' : undefined,
-            }),
-            route,
-        });
-        if (this.isDevModeApp(app)) {
-            config.apiOptions.middleware.push({
-                handler: createProxyHandler({
-                    hostname: this.options.hostname,
-                    port,
-                    route: 'sockjs-node',
-                    label: 'Admin UI live reload',
-                    basePath: 'sockjs-node',
-                }),
-                route: 'sockjs-node',
-            });
-        }
-        return config;
-    }
-
-    /** @internal */
-    async onVendureBootstrap() {
-        const { apiHost, apiPort, port, app, adminUiConfig } = AdminUiPlugin.options;
-        // TODO: Remove in next minor version (0.11.0)
-        if (apiHost || apiPort) {
-            Logger.warn(
-                `The "apiHost" and "apiPort" options are deprecated and will be removed in a future version.`,
-                loggerCtx,
-            );
-            Logger.warn(
-                `Use the "adminUiConfig.apiHost", "adminUiConfig.apiPort" properties instead.`,
-                loggerCtx,
-            );
-        }
+    async configure(consumer: MiddlewareConsumer) {
+        const { app, hostname, port, route, adminUiConfig } = AdminUiPlugin.options;
         const adminUiAppPath = AdminUiPlugin.isDevModeApp(app)
         const adminUiAppPath = AdminUiPlugin.isDevModeApp(app)
             ? path.join(app.sourcePath, 'src')
             ? path.join(app.sourcePath, 'src')
             : (app && app.path) || DEFAULT_APP_PATH;
             : (app && app.path) || DEFAULT_APP_PATH;
@@ -186,14 +111,45 @@ export class AdminUiPlugin implements OnVendureBootstrap, OnVendureClose {
             return this.overwriteAdminUiConfig(adminUiConfigPath, uiConfig);
             return this.overwriteAdminUiConfig(adminUiConfigPath, uiConfig);
         };
         };
 
 
-        if (!AdminUiPlugin.isDevModeApp(app)) {
-            // If not in dev mode, start a static server for the compiled app
-            const adminUiServer = express();
-            adminUiServer.use(express.static(adminUiAppPath));
-            adminUiServer.use((req, res) => {
-                res.sendFile(path.join(adminUiAppPath, 'index.html'));
-            });
-            this.server = adminUiServer.listen(AdminUiPlugin.options.port);
+        if (AdminUiPlugin.isDevModeApp(app)) {
+            Logger.info('Creating admin ui middleware (dev mode)', loggerCtx);
+            consumer
+                .apply(
+                    createProxyHandler({
+                        hostname,
+                        port,
+                        route,
+                        label: 'Admin UI',
+                        basePath: route,
+                    }),
+                )
+                .forRoutes(route);
+            consumer
+                .apply(
+                    createProxyHandler({
+                        hostname,
+                        port,
+                        route: 'sockjs-node',
+                        label: 'Admin UI live reload',
+                        basePath: 'sockjs-node',
+                    }),
+                )
+                .forRoutes('sockjs-node');
+
+            Logger.info(`Compiling Admin UI app in development mode`, loggerCtx);
+            app.compile().then(
+                () => {
+                    Logger.info(`Admin UI compiling and watching for changes...`, loggerCtx);
+                },
+                (err: any) => {
+                    Logger.error(`Failed to compile: ${err}`, loggerCtx, err.stack);
+                },
+            );
+            await overwriteConfig();
+        } else {
+            Logger.info('Creating admin ui middleware (prod mode)', loggerCtx);
+            consumer.apply(await this.createStaticServer(app)).forRoutes(route);
+
             if (app && typeof app.compile === 'function') {
             if (app && typeof app.compile === 'function') {
                 Logger.info(`Compiling Admin UI app in production mode...`, loggerCtx);
                 Logger.info(`Compiling Admin UI app in production mode...`, loggerCtx);
                 app.compile()
                 app.compile()
@@ -209,25 +165,19 @@ export class AdminUiPlugin implements OnVendureBootstrap, OnVendureClose {
             } else {
             } else {
                 await overwriteConfig();
                 await overwriteConfig();
             }
             }
-        } else {
-            Logger.info(`Compiling Admin UI app in development mode`, loggerCtx);
-            app.compile().then(
-                () => {
-                    Logger.info(`Admin UI compiling and watching for changes...`, loggerCtx);
-                },
-                (err: any) => {
-                    Logger.error(`Failed to compile: ${err}`, loggerCtx, err.stack);
-                },
-            );
-            await overwriteConfig();
         }
         }
     }
     }
 
 
-    /** @internal */
-    async onVendureClose(): Promise<void> {
-        if (this.server) {
-            await new Promise(resolve => this.server.close(() => resolve()));
-        }
+    private async createStaticServer(app?: AdminUiAppConfig) {
+        const adminUiAppPath = (app && app.path) || DEFAULT_APP_PATH;
+
+        const adminUiServer = express.Router();
+        adminUiServer.use(express.static(adminUiAppPath));
+        adminUiServer.use((req, res) => {
+            res.sendFile(path.join(adminUiAppPath, 'index.html'));
+        });
+
+        return adminUiServer;
     }
     }
 
 
     /**
     /**
@@ -245,8 +195,8 @@ export class AdminUiPlugin implements OnVendureBootstrap, OnVendureClose {
         };
         };
         return {
         return {
             adminApiPath: propOrDefault('adminApiPath', this.configService.apiOptions.adminApiPath),
             adminApiPath: propOrDefault('adminApiPath', this.configService.apiOptions.adminApiPath),
-            apiHost: propOrDefault('apiHost', AdminUiPlugin.options.apiHost || 'auto'),
-            apiPort: propOrDefault('apiPort', AdminUiPlugin.options.apiPort || 'auto'),
+            apiHost: propOrDefault('apiHost', 'auto'),
+            apiPort: propOrDefault('apiPort', 'auto'),
             tokenMethod: propOrDefault('tokenMethod', authOptions.tokenMethod || 'cookie'),
             tokenMethod: propOrDefault('tokenMethod', authOptions.tokenMethod || 'cookie'),
             authTokenHeaderKey: propOrDefault(
             authTokenHeaderKey: propOrDefault(
                 'authTokenHeaderKey',
                 'authTokenHeaderKey',

+ 11 - 41
packages/asset-server-plugin/src/plugin.ts

@@ -1,12 +1,8 @@
-import { DNSHealthIndicator, TerminusModule } from '@nestjs/terminus';
+import { MiddlewareConsumer, NestModule, OnApplicationBootstrap } from '@nestjs/common';
 import { Type } from '@vendure/common/lib/shared-types';
 import { Type } from '@vendure/common/lib/shared-types';
 import {
 import {
     AssetStorageStrategy,
     AssetStorageStrategy,
-    createProxyHandler,
-    HealthCheckRegistryService,
     Logger,
     Logger,
-    OnVendureBootstrap,
-    OnVendureClose,
     PluginCommonModule,
     PluginCommonModule,
     RuntimeVendureConfig,
     RuntimeVendureConfig,
     VendurePlugin,
     VendurePlugin,
@@ -15,7 +11,6 @@ import { createHash } from 'crypto';
 import express, { NextFunction, Request, Response } from 'express';
 import express, { NextFunction, Request, Response } from 'express';
 import { fromBuffer } from 'file-type';
 import { fromBuffer } from 'file-type';
 import fs from 'fs-extra';
 import fs from 'fs-extra';
-import { Server } from 'http';
 import path from 'path';
 import path from 'path';
 
 
 import { loggerCtx } from './constants';
 import { loggerCtx } from './constants';
@@ -124,11 +119,10 @@ import { AssetServerOptions, ImageTransformPreset } from './types';
  * @docsCategory AssetServerPlugin
  * @docsCategory AssetServerPlugin
  */
  */
 @VendurePlugin({
 @VendurePlugin({
-    imports: [PluginCommonModule, TerminusModule],
+    imports: [PluginCommonModule],
     configuration: config => AssetServerPlugin.configure(config),
     configuration: config => AssetServerPlugin.configure(config),
 })
 })
-export class AssetServerPlugin implements OnVendureBootstrap, OnVendureClose {
-    private server: Server;
+export class AssetServerPlugin implements NestModule, OnApplicationBootstrap {
     private static assetStorage: AssetStorageStrategy;
     private static assetStorage: AssetStorageStrategy;
     private readonly cacheDir = 'cache';
     private readonly cacheDir = 'cache';
     private presets: ImageTransformPreset[] = [
     private presets: ImageTransformPreset[] = [
@@ -140,11 +134,6 @@ export class AssetServerPlugin implements OnVendureBootstrap, OnVendureClose {
     ];
     ];
     private static options: AssetServerOptions;
     private static options: AssetServerOptions;
 
 
-    constructor(
-        private healthCheckRegistryService: HealthCheckRegistryService,
-        private dns: DNSHealthIndicator,
-    ) {}
-
     /**
     /**
      * @description
      * @description
      * Set the plugin options.
      * Set the plugin options.
@@ -166,15 +155,11 @@ export class AssetServerPlugin implements OnVendureBootstrap, OnVendureClose {
         config.assetOptions.assetStorageStrategy = this.assetStorage;
         config.assetOptions.assetStorageStrategy = this.assetStorage;
         config.assetOptions.assetNamingStrategy =
         config.assetOptions.assetNamingStrategy =
             this.options.namingStrategy || new HashedAssetNamingStrategy();
             this.options.namingStrategy || new HashedAssetNamingStrategy();
-        config.apiOptions.middleware.push({
-            handler: createProxyHandler({ ...this.options, label: 'Asset Server' }),
-            route: this.options.route,
-        });
         return config;
         return config;
     }
     }
 
 
     /** @internal */
     /** @internal */
-    onVendureBootstrap(): void | Promise<void> {
+    onApplicationBootstrap(): void | Promise<void> {
         if (AssetServerPlugin.options.presets) {
         if (AssetServerPlugin.options.presets) {
             for (const preset of AssetServerPlugin.options.presets) {
             for (const preset of AssetServerPlugin.options.presets) {
                 const existingIndex = this.presets.findIndex(p => p.name === preset.name);
                 const existingIndex = this.presets.findIndex(p => p.name === preset.name);
@@ -188,37 +173,20 @@ export class AssetServerPlugin implements OnVendureBootstrap, OnVendureClose {
 
 
         const cachePath = path.join(AssetServerPlugin.options.assetUploadDir, this.cacheDir);
         const cachePath = path.join(AssetServerPlugin.options.assetUploadDir, this.cacheDir);
         fs.ensureDirSync(cachePath);
         fs.ensureDirSync(cachePath);
-        this.createAssetServer();
-        const { hostname, port } = AssetServerPlugin.options;
     }
     }
 
 
-    /** @internal */
-    onVendureClose(): Promise<void> {
-        return new Promise(resolve => {
-            this.server.close(() => resolve());
-        });
+    configure(consumer: MiddlewareConsumer) {
+        Logger.info('Creating asset server middleware', loggerCtx);
+        consumer.apply(this.createAssetServer()).forRoutes(AssetServerPlugin.options.route);
     }
     }
 
 
     /**
     /**
      * Creates the image server instance
      * Creates the image server instance
      */
      */
     private createAssetServer() {
     private createAssetServer() {
-        const assetServer = express();
-        assetServer.get('/health', (req, res) => {
-            res.send('ok');
-        });
+        const assetServer = express.Router();
         assetServer.use(this.sendAsset(), this.generateTransformedImage());
         assetServer.use(this.sendAsset(), this.generateTransformedImage());
-
-        this.server = assetServer.listen(AssetServerPlugin.options.port, () => {
-            const addressInfo = this.server.address();
-            if (addressInfo && typeof addressInfo !== 'string') {
-                const { address, port } = addressInfo;
-                Logger.info(`Asset server listening on "http://localhost:${port}"`, loggerCtx);
-                this.healthCheckRegistryService.registerIndicatorFunction(() =>
-                    this.dns.pingCheck('asset-server', `http://localhost:${port}/health`),
-                );
-            }
-        });
+        return assetServer;
     }
     }
 
 
     /**
     /**
@@ -273,9 +241,11 @@ export class AssetServerPlugin implements OnVendureBootstrap, OnVendureClose {
                         }
                         }
                         res.set('Content-Type', `image/${(await image.metadata()).format}`);
                         res.set('Content-Type', `image/${(await image.metadata()).format}`);
                         res.send(imageBuffer);
                         res.send(imageBuffer);
+                        return;
                     } catch (e) {
                     } catch (e) {
                         Logger.error(e, 'AssetServerPlugin', e.stack);
                         Logger.error(e, 'AssetServerPlugin', e.stack);
                         res.status(500).send(e.message);
                         res.status(500).send(e.message);
+                        return;
                     }
                     }
                 }
                 }
             }
             }

+ 1 - 9
packages/asset-server-plugin/src/types.ts

@@ -44,17 +44,9 @@ export interface ImageTransformPreset {
  * @docsCategory AssetServerPlugin
  * @docsCategory AssetServerPlugin
  */
  */
 export interface AssetServerOptions {
 export interface AssetServerOptions {
-    hostname?: string;
     /**
     /**
      * @description
      * @description
-     * The local port that the server will run on. Note that the AssetServerPlugin
-     * includes a proxy server which allows the asset server to be accessed on the same
-     * port as the main Vendure server.
-     */
-    port: number;
-    /**
-     * @description
-     * The proxy route to the asset server.
+     * The route to the asset server.
      */
      */
     route: string;
     route: string;
     /**
     /**

+ 1 - 0
packages/core/e2e/collection.e2e-spec.ts

@@ -3,6 +3,7 @@ import { ROOT_COLLECTION_NAME } from '@vendure/common/lib/shared-constants';
 import {
 import {
     DefaultJobQueuePlugin,
     DefaultJobQueuePlugin,
     facetValueCollectionFilter,
     facetValueCollectionFilter,
+    JobQueueService,
     variantNameCollectionFilter,
     variantNameCollectionFilter,
 } from '@vendure/core';
 } from '@vendure/core';
 import { createTestEnvironment } from '@vendure/testing';
 import { createTestEnvironment } from '@vendure/testing';

+ 6 - 1
packages/core/e2e/default-search-plugin.e2e-spec.ts

@@ -4,6 +4,7 @@ import {
     DefaultJobQueuePlugin,
     DefaultJobQueuePlugin,
     DefaultSearchPlugin,
     DefaultSearchPlugin,
     facetValueCollectionFilter,
     facetValueCollectionFilter,
+    JobQueueService,
     mergeConfig,
     mergeConfig,
 } from '@vendure/core';
 } from '@vendure/core';
 import { createTestEnvironment, E2E_DEFAULT_CHANNEL_TOKEN, SimpleGraphQLClient } from '@vendure/testing';
 import { createTestEnvironment, E2E_DEFAULT_CHANNEL_TOKEN, SimpleGraphQLClient } from '@vendure/testing';
@@ -65,6 +66,10 @@ import {
 import { SEARCH_PRODUCTS_SHOP } from './graphql/shop-definitions';
 import { SEARCH_PRODUCTS_SHOP } from './graphql/shop-definitions';
 import { awaitRunningJobs } from './utils/await-running-jobs';
 import { awaitRunningJobs } from './utils/await-running-jobs';
 
 
+// Some of these tests have many steps and can timeout
+// on the default of 5s.
+jest.setTimeout(10000);
+
 describe('Default search plugin', () => {
 describe('Default search plugin', () => {
     const { server, adminClient, shopClient } = createTestEnvironment(
     const { server, adminClient, shopClient } = createTestEnvironment(
         mergeConfig(testConfig, {
         mergeConfig(testConfig, {
@@ -650,7 +655,7 @@ describe('Default search plugin', () => {
                     'Football',
                     'Football',
                     'Running Shoe',
                     'Running Shoe',
                 ]);
                 ]);
-            });
+            }, 10000);
 
 
             it('updates index when a Collection created', async () => {
             it('updates index when a Collection created', async () => {
                 const { createCollection } = await adminClient.query<
                 const { createCollection } = await adminClient.query<

+ 3 - 3
packages/core/e2e/fixtures/test-plugins/list-query-plugin.ts

@@ -1,3 +1,4 @@
+import { OnApplicationBootstrap } from '@nestjs/common';
 import { Args, Query, Resolver } from '@nestjs/graphql';
 import { Args, Query, Resolver } from '@nestjs/graphql';
 import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { DeepPartial, ID } from '@vendure/common/lib/shared-types';
 import { DeepPartial, ID } from '@vendure/common/lib/shared-types';
@@ -5,7 +6,6 @@ import {
     Ctx,
     Ctx,
     ListQueryBuilder,
     ListQueryBuilder,
     LocaleString,
     LocaleString,
-    OnVendureBootstrap,
     PluginCommonModule,
     PluginCommonModule,
     RequestContext,
     RequestContext,
     TransactionalConnection,
     TransactionalConnection,
@@ -153,10 +153,10 @@ const adminApiExtensions = gql`
         resolvers: [ListQueryResolver],
         resolvers: [ListQueryResolver],
     },
     },
 })
 })
-export class ListQueryPlugin implements OnVendureBootstrap {
+export class ListQueryPlugin implements OnApplicationBootstrap {
     constructor(private connection: TransactionalConnection) {}
     constructor(private connection: TransactionalConnection) {}
 
 
-    async onVendureBootstrap() {
+    async onApplicationBootstrap() {
         const count = await this.connection.getRepository(TestEntity).count();
         const count = await this.connection.getRepository(TestEntity).count();
         if (count === 0) {
         if (count === 0) {
             const testEntities = await this.connection.getRepository(TestEntity).save([
             const testEntities = await this.connection.getRepository(TestEntity).save([

+ 2 - 59
packages/core/e2e/fixtures/test-plugins/with-all-lifecycle-hooks.ts

@@ -1,37 +1,10 @@
 import { INestApplication, INestMicroservice } from '@nestjs/common';
 import { INestApplication, INestMicroservice } from '@nestjs/common';
-import {
-    OnVendureBootstrap,
-    OnVendureClose,
-    OnVendureWorkerBootstrap,
-    OnVendureWorkerClose,
-} from '@vendure/core';
 
 
-export class TestPluginWithAllLifecycleHooks
-    implements OnVendureBootstrap, OnVendureWorkerBootstrap, OnVendureClose, OnVendureWorkerClose {
+export class TestPluginWithAllLifecycleHooks {
     private static onConstructorFn: any;
     private static onConstructorFn: any;
-    private static onBeforeBootstrapFn: any;
-    private static onBeforeWorkerBootstrapFn: any;
-    private static onBootstrapFn: any;
-    private static onWorkerBootstrapFn: any;
-    private static onCloseFn: any;
-    private static onWorkerCloseFn: any;
 
 
-    static init(
-        constructorFn: any,
-        beforeBootstrapFn: any,
-        beforeWorkerBootstrapFn: any,
-        bootstrapFn: any,
-        workerBootstrapFn: any,
-        closeFn: any,
-        workerCloseFn: any,
-    ) {
+    static init(constructorFn: any) {
         this.onConstructorFn = constructorFn;
         this.onConstructorFn = constructorFn;
-        this.onBeforeBootstrapFn = beforeBootstrapFn;
-        this.onBeforeWorkerBootstrapFn = beforeWorkerBootstrapFn;
-        this.onBootstrapFn = bootstrapFn;
-        this.onWorkerBootstrapFn = workerBootstrapFn;
-        this.onCloseFn = closeFn;
-        this.onWorkerCloseFn = workerCloseFn;
         return this;
         return this;
     }
     }
 
 
@@ -39,32 +12,6 @@ export class TestPluginWithAllLifecycleHooks
         TestPluginWithAllLifecycleHooks.onConstructorFn();
         TestPluginWithAllLifecycleHooks.onConstructorFn();
     }
     }
 
 
-    static beforeVendureBootstrap(app: INestApplication): void | Promise<void> {
-        TestPluginWithAllLifecycleHooks.onBeforeBootstrapFn(app);
-    }
-
-    static beforeVendureWorkerBootstrap(app: INestMicroservice): void | Promise<void> {
-        TestPluginWithAllLifecycleHooks.onBeforeWorkerBootstrapFn(app);
-    }
-
-    onVendureBootstrap(): void | Promise<void> {
-        TestPluginWithAllLifecycleHooks.onBootstrapFn();
-    }
-
-    onVendureWorkerBootstrap(): void | Promise<void> {
-        TestPluginWithAllLifecycleHooks.onWorkerBootstrapFn();
-    }
-
-    onVendureClose(): void | Promise<void> {
-        TestPluginWithAllLifecycleHooks.onCloseFn();
-        this.resetSpies();
-    }
-
-    onVendureWorkerClose(): void | Promise<void> {
-        TestPluginWithAllLifecycleHooks.onWorkerCloseFn();
-        this.resetSpies();
-    }
-
     /**
     /**
      * This is required because on the first run, the Vendure server will be bootstrapped twice -
      * This is required because on the first run, the Vendure server will be bootstrapped twice -
      * once to populate the database and the second time for the actual tests. Thus the call counts
      * once to populate the database and the second time for the actual tests. Thus the call counts
@@ -73,9 +20,5 @@ export class TestPluginWithAllLifecycleHooks
      */
      */
     private resetSpies() {
     private resetSpies() {
         TestPluginWithAllLifecycleHooks.onConstructorFn.mockClear();
         TestPluginWithAllLifecycleHooks.onConstructorFn.mockClear();
-        TestPluginWithAllLifecycleHooks.onBeforeBootstrapFn.mockClear();
-        TestPluginWithAllLifecycleHooks.onBeforeWorkerBootstrapFn.mockClear();
-        TestPluginWithAllLifecycleHooks.onBootstrapFn.mockClear();
-        TestPluginWithAllLifecycleHooks.onWorkerBootstrapFn.mockClear();
     }
     }
 }
 }

+ 0 - 35
packages/core/e2e/fixtures/test-plugins/with-config-and-bootstrap.ts

@@ -1,35 +0,0 @@
-import { LanguageCode } from '@vendure/common/lib/generated-types';
-import {
-    ConfigModule,
-    ConfigService,
-    OnVendureBootstrap,
-    OnVendureClose,
-    VendurePlugin,
-} from '@vendure/core';
-
-@VendurePlugin({
-    imports: [ConfigModule],
-    configuration: (config) => {
-        // tslint:disable-next-line:no-non-null-assertion
-        config.defaultLanguageCode = LanguageCode.zh;
-        return config;
-    },
-})
-export class TestPluginWithConfigAndBootstrap implements OnVendureBootstrap, OnVendureClose {
-    private static boostrapWasCalled: any;
-
-    static setup(boostrapWasCalled: (arg: any) => void) {
-        TestPluginWithConfigAndBootstrap.boostrapWasCalled = boostrapWasCalled;
-        return TestPluginWithConfigAndBootstrap;
-    }
-
-    constructor(private configService: ConfigService) {}
-
-    onVendureBootstrap() {
-        TestPluginWithConfigAndBootstrap.boostrapWasCalled(this.configService);
-    }
-
-    onVendureClose() {
-        TestPluginWithConfigAndBootstrap.boostrapWasCalled.mockClear();
-    }
-}

+ 22 - 0
packages/core/e2e/fixtures/test-plugins/with-config.ts

@@ -0,0 +1,22 @@
+import { LanguageCode } from '@vendure/common/lib/generated-types';
+import {
+    ConfigModule,
+    ConfigService,
+    OnVendureBootstrap,
+    OnVendureClose,
+    VendurePlugin,
+} from '@vendure/core';
+
+@VendurePlugin({
+    imports: [ConfigModule],
+    configuration: config => {
+        // tslint:disable-next-line:no-non-null-assertion
+        config.defaultLanguageCode = LanguageCode.zh;
+        return config;
+    },
+})
+export class TestPluginWithConfig {
+    static setup() {
+        return TestPluginWithConfig;
+    }
+}

+ 2 - 2
packages/core/e2e/fixtures/test-plugins/with-job-queue.ts

@@ -9,8 +9,8 @@ class TestController implements OnModuleInit {
 
 
     constructor(private jobQueueService: JobQueueService) {}
     constructor(private jobQueueService: JobQueueService) {}
 
 
-    onModuleInit(): any {
-        this.queue = this.jobQueueService.createQueue({
+    async onModuleInit(): Promise<void> {
+        this.queue = await this.jobQueueService.createQueue({
             name: 'test',
             name: 'test',
             process: job => {
             process: job => {
                 return PluginWithJobQueue.jobSubject
                 return PluginWithJobQueue.jobSubject

+ 0 - 46
packages/core/e2e/fixtures/test-plugins/with-worker-controller.ts

@@ -1,46 +0,0 @@
-import { Controller, Get } from '@nestjs/common';
-import { MessagePattern } from '@nestjs/microservices';
-import {
-    PluginCommonModule,
-    ProcessContext,
-    VendurePlugin,
-    WorkerMessage,
-    WorkerService,
-} from '@vendure/core';
-import { of } from 'rxjs';
-
-class TestWorkerMessage extends WorkerMessage<string, boolean> {
-    static readonly pattern = 'TestWorkerMessage';
-}
-
-@Controller('process-context')
-export class TestProcessContextController {
-    constructor(private processContext: ProcessContext, private workerService: WorkerService) {}
-
-    @Get('server')
-    isServer() {
-        return this.processContext.isServer;
-    }
-
-    @Get('worker')
-    isWorker() {
-        return this.workerService.send(new TestWorkerMessage('hello'));
-    }
-}
-
-@Controller()
-export class TestProcessContextWorkerController {
-    constructor(private processContext: ProcessContext) {}
-
-    @MessagePattern(TestWorkerMessage.pattern)
-    isWorker() {
-        return of(this.processContext.isWorker);
-    }
-}
-
-@VendurePlugin({
-    imports: [PluginCommonModule],
-    controllers: [TestProcessContextController],
-    workers: [TestProcessContextWorkerController],
-})
-export class TestProcessContextPlugin {}

+ 0 - 3
packages/core/e2e/job-queue.e2e-spec.ts

@@ -24,9 +24,6 @@ describe('JobQueue', () => {
     const { server, adminClient } = createTestEnvironment(
     const { server, adminClient } = createTestEnvironment(
         mergeConfig(testConfig, {
         mergeConfig(testConfig, {
             plugins: [DefaultJobQueuePlugin, PluginWithJobQueue],
             plugins: [DefaultJobQueuePlugin, PluginWithJobQueue],
-            workerOptions: {
-                runInMainProcess: true,
-            },
         }),
         }),
     );
     );
 
 

+ 1 - 1
packages/core/e2e/order-item-price-calculation-strategy.e2e-spec.ts

@@ -1,4 +1,4 @@
-import { DefaultSearchPlugin, mergeConfig } from '@vendure/core';
+import { DefaultSearchPlugin, JobQueueService, mergeConfig } from '@vendure/core';
 import { createTestEnvironment } from '@vendure/testing';
 import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';

+ 5 - 79
packages/core/e2e/plugin.e2e-spec.ts

@@ -5,44 +5,26 @@ import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
 import { initialData } from '../../../e2e-common/e2e-initial-data';
-import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
+import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
 
 
 import { TestPluginWithAllLifecycleHooks } from './fixtures/test-plugins/with-all-lifecycle-hooks';
 import { TestPluginWithAllLifecycleHooks } from './fixtures/test-plugins/with-all-lifecycle-hooks';
 import { TestAPIExtensionPlugin } from './fixtures/test-plugins/with-api-extensions';
 import { TestAPIExtensionPlugin } from './fixtures/test-plugins/with-api-extensions';
-import { TestPluginWithConfigAndBootstrap } from './fixtures/test-plugins/with-config-and-bootstrap';
+import { TestPluginWithConfig } from './fixtures/test-plugins/with-config';
 import { TestLazyExtensionPlugin } from './fixtures/test-plugins/with-lazy-api-extensions';
 import { TestLazyExtensionPlugin } from './fixtures/test-plugins/with-lazy-api-extensions';
 import { TestPluginWithProvider } from './fixtures/test-plugins/with-provider';
 import { TestPluginWithProvider } from './fixtures/test-plugins/with-provider';
 import { TestRestPlugin } from './fixtures/test-plugins/with-rest-controller';
 import { TestRestPlugin } from './fixtures/test-plugins/with-rest-controller';
-import { TestProcessContextPlugin } from './fixtures/test-plugins/with-worker-controller';
 
 
 describe('Plugins', () => {
 describe('Plugins', () => {
-    const bootstrapMockFn = jest.fn();
     const onConstructorFn = jest.fn();
     const onConstructorFn = jest.fn();
-    const beforeBootstrapFn = jest.fn();
-    const beforeWorkerBootstrapFn = jest.fn();
-    const onBootstrapFn = jest.fn();
-    const onWorkerBootstrapFn = jest.fn();
-    const onCloseFn = jest.fn();
-    const onWorkerCloseFn = jest.fn();
-
     const { server, adminClient, shopClient } = createTestEnvironment({
     const { server, adminClient, shopClient } = createTestEnvironment({
         ...testConfig,
         ...testConfig,
         plugins: [
         plugins: [
-            TestPluginWithAllLifecycleHooks.init(
-                onConstructorFn,
-                beforeBootstrapFn,
-                beforeWorkerBootstrapFn,
-                onBootstrapFn,
-                onWorkerBootstrapFn,
-                onCloseFn,
-                onWorkerCloseFn,
-            ),
-            TestPluginWithConfigAndBootstrap.setup(bootstrapMockFn),
+            TestPluginWithAllLifecycleHooks.init(onConstructorFn),
+            TestPluginWithConfig.setup(),
             TestAPIExtensionPlugin,
             TestAPIExtensionPlugin,
             TestPluginWithProvider,
             TestPluginWithProvider,
             TestLazyExtensionPlugin,
             TestLazyExtensionPlugin,
             TestRestPlugin,
             TestRestPlugin,
-            TestProcessContextPlugin,
         ],
         ],
     });
     });
 
 
@@ -59,31 +41,8 @@ describe('Plugins', () => {
         await server.destroy();
         await server.destroy();
     });
     });
 
 
-    it('constructs one instance for each process', () => {
-        expect(onConstructorFn).toHaveBeenCalledTimes(2);
-    });
-
-    it('calls beforeVendureBootstrap', () => {
-        expect(beforeBootstrapFn).toHaveBeenCalledTimes(1);
-        expect(beforeBootstrapFn).toHaveBeenCalledWith(server.app);
-    });
-
-    it('calls beforeVendureWorkerBootstrap', () => {
-        expect(beforeWorkerBootstrapFn).toHaveBeenCalledTimes(1);
-        expect(beforeWorkerBootstrapFn).toHaveBeenCalledWith(server.worker);
-    });
-
-    it('calls onVendureBootstrap', () => {
-        expect(onBootstrapFn).toHaveBeenCalledTimes(1);
-    });
-
-    it('calls onWorkerVendureBootstrap', () => {
-        expect(onWorkerBootstrapFn).toHaveBeenCalledTimes(1);
-    });
-
     it('can modify the config in configure()', () => {
     it('can modify the config in configure()', () => {
-        expect(bootstrapMockFn).toHaveBeenCalledTimes(1);
-        const configService: ConfigService = bootstrapMockFn.mock.calls[0][0];
+        const configService = server.app.get(ConfigService);
         expect(configService instanceof ConfigService).toBe(true);
         expect(configService instanceof ConfigService).toBe(true);
         expect(configService.defaultLanguageCode).toBe(LanguageCode.zh);
         expect(configService.defaultLanguageCode).toBe(LanguageCode.zh);
     });
     });
@@ -174,37 +133,4 @@ describe('Plugins', () => {
             expect(result.message).toContain('uh oh!');
             expect(result.message).toContain('uh oh!');
         });
         });
     });
     });
-
-    describe('processContext', () => {
-        it('server context', async () => {
-            const response = await shopClient.fetch(
-                `http://localhost:${testConfig.apiOptions.port}/process-context/server`,
-            );
-            const body = await response.text();
-
-            expect(body).toBe('true');
-        });
-        it('worker context', async () => {
-            const response = await shopClient.fetch(
-                `http://localhost:${testConfig.apiOptions.port}/process-context/worker`,
-            );
-            const body = await response.text();
-
-            expect(body).toBe('true');
-        });
-    });
-
-    describe('on app close', () => {
-        beforeAll(async () => {
-            await server.destroy();
-        });
-
-        it('calls onVendureClose', () => {
-            expect(onCloseFn).toHaveBeenCalled();
-        });
-
-        it('calls onWorkerVendureClose', () => {
-            expect(onWorkerCloseFn).toHaveBeenCalled();
-        });
-    });
 });
 });

+ 1 - 1
packages/core/e2e/shop-catalog.e2e-spec.ts

@@ -1,5 +1,5 @@
 /* tslint:disable:no-non-null-assertion */
 /* tslint:disable:no-non-null-assertion */
-import { facetValueCollectionFilter } from '@vendure/core';
+import { facetValueCollectionFilter, JobQueueService } from '@vendure/core';
 import { createTestEnvironment } from '@vendure/testing';
 import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';

+ 1 - 1
packages/core/src/api/common/request-context.ts

@@ -113,7 +113,7 @@ export class RequestContext {
      * @description
      * @description
      * Serializes the RequestContext object into a JSON-compatible simple object.
      * Serializes the RequestContext object into a JSON-compatible simple object.
      * This is useful when you need to send a RequestContext object to another
      * This is useful when you need to send a RequestContext object to another
-     * process, e.g. to pass it to the Worker process via the {@link WorkerService}.
+     * process, e.g. to pass it to the Job Queue via the {@link JobQueueService}.
      */
      */
     serialize(): SerializedRequestContext {
     serialize(): SerializedRequestContext {
         const serializableThis: any = Object.assign({}, this);
         const serializableThis: any = Object.assign({}, this);

+ 1 - 9
packages/core/src/app.module.ts

@@ -9,17 +9,9 @@ import { HealthCheckModule } from './health-check/health-check.module';
 import { I18nModule } from './i18n/i18n.module';
 import { I18nModule } from './i18n/i18n.module';
 import { I18nService } from './i18n/i18n.service';
 import { I18nService } from './i18n/i18n.service';
 import { PluginModule } from './plugin/plugin.module';
 import { PluginModule } from './plugin/plugin.module';
-import { ProcessContextModule } from './process-context/process-context.module';
 
 
 @Module({
 @Module({
-    imports: [
-        ConfigModule,
-        I18nModule,
-        ApiModule,
-        PluginModule.forRoot(),
-        ProcessContextModule.forRoot(),
-        HealthCheckModule,
-    ],
+    imports: [ConfigModule, I18nModule, ApiModule, PluginModule.forRoot(), HealthCheckModule],
 })
 })
 export class AppModule implements NestModule, OnApplicationShutdown {
 export class AppModule implements NestModule, OnApplicationShutdown {
     constructor(private configService: ConfigService, private i18nService: I18nService) {}
     constructor(private configService: ConfigService, private i18nService: I18nService) {}

+ 0 - 0
packages/core/src/worker/async-observable.ts → packages/core/src/async/async-observable.ts


+ 1 - 0
packages/core/src/async/index.ts

@@ -0,0 +1 @@
+export * from './async-observable';

+ 8 - 115
packages/core/src/bootstrap.ts

@@ -1,6 +1,5 @@
-import { INestApplication, INestMicroservice } from '@nestjs/common';
+import { INestApplication, INestApplicationContext } from '@nestjs/common';
 import { NestFactory } from '@nestjs/core';
 import { NestFactory } from '@nestjs/core';
-import { TcpClientOptions, Transport } from '@nestjs/microservices';
 import { getConnectionToken } from '@nestjs/typeorm';
 import { getConnectionToken } from '@nestjs/typeorm';
 import { Type } from '@vendure/common/lib/shared-types';
 import { Type } from '@vendure/common/lib/shared-types';
 import cookieSession = require('cookie-session');
 import cookieSession = require('cookie-session');
@@ -18,7 +17,6 @@ import { setEntityIdStrategy } from './entity/set-entity-id-strategy';
 import { validateCustomFieldsConfig } from './entity/validate-custom-fields-config';
 import { validateCustomFieldsConfig } from './entity/validate-custom-fields-config';
 import { getConfigurationFunction, getEntitiesFromPlugins } from './plugin/plugin-metadata';
 import { getConfigurationFunction, getEntitiesFromPlugins } from './plugin/plugin-metadata';
 import { getProxyMiddlewareCliGreetings } from './plugin/plugin-utils';
 import { getProxyMiddlewareCliGreetings } from './plugin/plugin-utils';
-import { BeforeVendureBootstrap, BeforeVendureWorkerBootstrap } from './plugin/vendure-plugin';
 
 
 export type VendureBootstrapFunction = (config: VendureConfig) => Promise<INestApplication>;
 export type VendureBootstrapFunction = (config: VendureConfig) => Promise<INestApplication>;
 
 
@@ -54,7 +52,6 @@ export async function bootstrap(userConfig: Partial<VendureConfig>): Promise<INe
     });
     });
     DefaultLogger.restoreOriginalLogLevel();
     DefaultLogger.restoreOriginalLogLevel();
     app.useLogger(new Logger());
     app.useLogger(new Logger());
-    await runBeforeBootstrapHooks(config, app);
     if (config.authOptions.tokenMethod === 'cookie') {
     if (config.authOptions.tokenMethod === 'cookie') {
         const { sessionSecret, cookieOptions } = config.authOptions;
         const { sessionSecret, cookieOptions } = config.authOptions;
         app.use(
         app.use(
@@ -67,24 +64,13 @@ export async function bootstrap(userConfig: Partial<VendureConfig>): Promise<INe
     }
     }
     await app.listen(port, hostname || '');
     await app.listen(port, hostname || '');
     app.enableShutdownHooks();
     app.enableShutdownHooks();
-    if (config.workerOptions.runInMainProcess) {
-        try {
-            const worker = await bootstrapWorkerInternal(config);
-            Logger.warn(`Worker is running in main process. This is not recommended for production.`);
-            Logger.warn(`[VendureConfig.workerOptions.runInMainProcess = true]`);
-            closeWorkerOnAppClose(app, worker);
-        } catch (e) {
-            Logger.error(`Could not start the worker process: ${e.message || e}`, 'Vendure Worker');
-        }
-    }
     logWelcomeMessage(config);
     logWelcomeMessage(config);
     return app;
     return app;
 }
 }
 
 
 /**
 /**
  * @description
  * @description
- * Bootstraps the Vendure worker. Read more about the [Vendure Worker]({{< relref "vendure-worker" >}}) or see the worker-specific options
- * defined in {@link WorkerOptions}.
+ * Bootstraps the Vendure . Read more about the [Vendure Worker]({{< relref "vendure-worker" >}})
  *
  *
  * @example
  * @example
  * ```TypeScript
  * ```TypeScript
@@ -97,59 +83,22 @@ export async function bootstrap(userConfig: Partial<VendureConfig>): Promise<INe
  * ```
  * ```
  * @docsCategory worker
  * @docsCategory worker
  * */
  * */
-export async function bootstrapWorker(userConfig: Partial<VendureConfig>): Promise<INestMicroservice> {
-    if (userConfig.workerOptions && userConfig.workerOptions.runInMainProcess === true) {
-        Logger.useLogger(userConfig.logger || new DefaultLogger());
-        const errorMessage = `Cannot bootstrap worker when "runInMainProcess" is set to true`;
-        Logger.error(errorMessage, 'Vendure Worker');
-        throw new Error(errorMessage);
-    } else {
-        try {
-            const vendureConfig = await preBootstrapConfig(userConfig);
-            return await bootstrapWorkerInternal(vendureConfig);
-        } catch (e) {
-            Logger.error(`Could not start the worker process: ${e.message}`, 'Vendure Worker');
-            throw e;
-        }
-    }
-}
-
-async function bootstrapWorkerInternal(
-    vendureConfig: Readonly<RuntimeVendureConfig>,
-): Promise<INestMicroservice> {
+export async function bootstrapWorker(userConfig: Partial<VendureConfig>): Promise<INestApplicationContext> {
+    const vendureConfig = await preBootstrapConfig(userConfig);
     const config = disableSynchronize(vendureConfig);
     const config = disableSynchronize(vendureConfig);
-    if (!config.workerOptions.runInMainProcess && (config.logger as any).setDefaultContext) {
-        (config.logger as any).setDefaultContext('Vendure Worker');
-    }
+    (config.logger as any).setDefaultContext('Vendure Worker');
     Logger.useLogger(config.logger);
     Logger.useLogger(config.logger);
     Logger.info(`Bootstrapping Vendure Worker (pid: ${process.pid})...`);
     Logger.info(`Bootstrapping Vendure Worker (pid: ${process.pid})...`);
 
 
-    const workerModule = await import('./worker/worker.module');
+    const appModule = await import('./app.module');
     DefaultLogger.hideNestBoostrapLogs();
     DefaultLogger.hideNestBoostrapLogs();
-    const workerApp = await NestFactory.createMicroservice(workerModule.WorkerModule, {
-        transport: config.workerOptions.transport,
+    const workerApp = await NestFactory.createApplicationContext(appModule.AppModule, {
         logger: new Logger(),
         logger: new Logger(),
-        options: config.workerOptions.options,
     });
     });
     DefaultLogger.restoreOriginalLogLevel();
     DefaultLogger.restoreOriginalLogLevel();
     workerApp.useLogger(new Logger());
     workerApp.useLogger(new Logger());
     workerApp.enableShutdownHooks();
     workerApp.enableShutdownHooks();
     await validateDbTablesForWorker(workerApp);
     await validateDbTablesForWorker(workerApp);
-    await runBeforeWorkerBootstrapHooks(config, workerApp);
-    // A work-around to correctly handle errors when attempting to start the
-    // microservice server listening.
-    // See https://github.com/nestjs/nest/issues/2777
-    // TODO: Remove if & when the above issue is resolved.
-    await new Promise((resolve, reject) => {
-        const tcpServer = (workerApp as any).server.server;
-        if (tcpServer) {
-            tcpServer.on('error', (e: any) => {
-                reject(e);
-            });
-        }
-        workerApp.listenAsync().then(resolve);
-    });
-    workerWelcomeMessage(config);
     return workerApp;
     return workerApp;
 }
 }
 
 
@@ -246,62 +195,6 @@ function setExposedHeaders(config: Readonly<RuntimeVendureConfig>) {
     }
     }
 }
 }
 
 
-export async function runBeforeBootstrapHooks(config: Readonly<RuntimeVendureConfig>, app: INestApplication) {
-    function hasBeforeBootstrapHook(
-        plugin: any,
-    ): plugin is { beforeVendureBootstrap: BeforeVendureBootstrap } {
-        return typeof plugin.beforeVendureBootstrap === 'function';
-    }
-    for (const plugin of config.plugins) {
-        if (hasBeforeBootstrapHook(plugin)) {
-            await plugin.beforeVendureBootstrap(app);
-        }
-    }
-}
-
-export async function runBeforeWorkerBootstrapHooks(
-    config: Readonly<RuntimeVendureConfig>,
-    worker: INestMicroservice,
-) {
-    function hasBeforeBootstrapHook(
-        plugin: any,
-    ): plugin is { beforeVendureWorkerBootstrap: BeforeVendureWorkerBootstrap } {
-        return typeof plugin.beforeVendureWorkerBootstrap === 'function';
-    }
-    for (const plugin of config.plugins) {
-        if (hasBeforeBootstrapHook(plugin)) {
-            await plugin.beforeVendureWorkerBootstrap(worker);
-        }
-    }
-}
-
-/**
- * Monkey-patches the app's .close() method to also close the worker microservice
- * instance too.
- */
-function closeWorkerOnAppClose(app: INestApplication, worker: INestMicroservice) {
-    // A Nest app is a nested Proxy. By getting the prototype we are
-    // able to access and override the actual close() method.
-    const appPrototype = Object.getPrototypeOf(app);
-    const appClose = appPrototype.close.bind(app);
-    appPrototype.close = async () => {
-        return Promise.all([appClose(), worker.close()]);
-    };
-}
-
-function workerWelcomeMessage(config: VendureConfig) {
-    let transportString = '';
-    let connectionString = '';
-    const transport = (config.workerOptions && config.workerOptions.transport) || Transport.TCP;
-    transportString = ` with ${Transport[transport]} transport`;
-    const options = (config.workerOptions as TcpClientOptions).options;
-    if (options) {
-        const { host, port } = options;
-        connectionString = ` at ${host || 'localhost'}:${port}`;
-    }
-    Logger.info(`Vendure Worker started${transportString}${connectionString}`);
-}
-
 function logWelcomeMessage(config: RuntimeVendureConfig) {
 function logWelcomeMessage(config: RuntimeVendureConfig) {
     let version: string;
     let version: string;
     try {
     try {
@@ -351,7 +244,7 @@ function disableSynchronize(userConfig: Readonly<RuntimeVendureConfig>): Readonl
  * before allowing the rest of the worker bootstrap to continue.
  * before allowing the rest of the worker bootstrap to continue.
  * @param worker
  * @param worker
  */
  */
-async function validateDbTablesForWorker(worker: INestMicroservice) {
+async function validateDbTablesForWorker(worker: INestApplicationContext) {
     const connection: Connection = worker.get(getConnectionToken());
     const connection: Connection = worker.get(getConnectionToken());
     await new Promise(async (resolve, reject) => {
     await new Promise(async (resolve, reject) => {
         const checkForTables = async (): Promise<boolean> => {
         const checkForTables = async (): Promise<boolean> => {

+ 9 - 0
packages/core/src/cache/cache.module.ts

@@ -0,0 +1,9 @@
+import { Module } from '@nestjs/common';
+
+import { RequestContextCacheService } from './request-context-cache.service';
+
+@Module({
+    providers: [RequestContextCacheService],
+    exports: [RequestContextCacheService],
+})
+export class CacheModule {}

+ 1 - 0
packages/core/src/cache/index.ts

@@ -0,0 +1 @@
+export * from './request-context-cache.service';

+ 38 - 0
packages/core/src/cache/request-context-cache.service.spec.ts

@@ -0,0 +1,38 @@
+import { RequestContext } from '../api';
+
+import { RequestContextCacheService } from './request-context-cache.service';
+
+describe('Request context cache', () => {
+    let cache: RequestContextCacheService;
+    beforeEach(() => {
+        cache = new RequestContextCacheService();
+    });
+
+    it('stores and retrieves a multiple values', async () => {
+        const ctx = RequestContext.empty();
+
+        await cache.set(ctx, 'test', 1);
+        await cache.set(ctx, 'test2', 2);
+        expect(await cache.get(ctx, 'test')).toBe(1);
+        expect(await cache.get(ctx, 'test2')).toBe(2);
+    });
+
+    it('can use objects as keys', async () => {
+        const ctx = RequestContext.empty();
+
+        const x = {};
+        await cache.set(ctx, x, 1);
+        expect(await cache.get(ctx, x)).toBe(1);
+    });
+
+    it('uses separate stores per context', async () => {
+        const ctx = RequestContext.empty();
+        const ctx2 = RequestContext.empty();
+
+        await cache.set(ctx, 'test', 1);
+        await cache.set(ctx2, 'test', 2);
+
+        expect(await cache.get(ctx, 'test')).toBe(1);
+        expect(await cache.get(ctx2, 'test')).toBe(2);
+    });
+});

+ 28 - 0
packages/core/src/cache/request-context-cache.service.ts

@@ -0,0 +1,28 @@
+import { RequestContext } from '../api';
+
+export class RequestContextCacheService {
+    private caches = new WeakMap<RequestContext, Map<any, any>>();
+
+    async set(ctx: RequestContext, key: any, val: any): Promise<void> {
+        this.getContextCache(ctx).set(key, val);
+    }
+
+    async get(ctx: RequestContext, key: any, getDefault?: () => Promise<any>): Promise<any> {
+        const ctxCache = this.getContextCache(ctx);
+        let result = ctxCache.get(key);
+        if (!result && getDefault) {
+            result = await getDefault();
+            ctxCache.set(key, result);
+        }
+        return result;
+    }
+
+    private getContextCache(ctx: RequestContext): Map<any, any> {
+        let ctxCache = this.caches.get(ctx);
+        if (!ctxCache) {
+            ctxCache = new Map<any, any>();
+            this.caches.set(ctx, ctxCache);
+        }
+        return ctxCache;
+    }
+}

+ 7 - 7
packages/core/src/cli/populate.ts

@@ -1,4 +1,4 @@
-import { INestApplication } from '@nestjs/common';
+import { INestApplicationContext } from '@nestjs/common';
 import fs from 'fs-extra';
 import fs from 'fs-extra';
 import path from 'path';
 import path from 'path';
 
 
@@ -12,11 +12,11 @@ import { logColored } from './cli-utils';
  *
  *
  * @docsCategory import-export
  * @docsCategory import-export
  */
  */
-export async function populate(
-    bootstrapFn: () => Promise<INestApplication | undefined>,
+export async function populate<T extends INestApplicationContext>(
+    bootstrapFn: () => Promise<T | undefined>,
     initialDataPathOrObject: string | object,
     initialDataPathOrObject: string | object,
     productsCsvPath?: string,
     productsCsvPath?: string,
-): Promise<INestApplication> {
+): Promise<T> {
     const app = await bootstrapFn();
     const app = await bootstrapFn();
     if (!app) {
     if (!app) {
         throw new Error('Could not bootstrap the Vendure app');
         throw new Error('Could not bootstrap the Vendure app');
@@ -48,7 +48,7 @@ export async function populate(
 }
 }
 
 
 export async function populateInitialData(
 export async function populateInitialData(
-    app: INestApplication,
+    app: INestApplicationContext,
     initialData: import('@vendure/core').InitialData,
     initialData: import('@vendure/core').InitialData,
     loggingFn?: (message: string) => void,
     loggingFn?: (message: string) => void,
 ) {
 ) {
@@ -65,7 +65,7 @@ export async function populateInitialData(
 }
 }
 
 
 export async function populateCollections(
 export async function populateCollections(
-    app: INestApplication,
+    app: INestApplicationContext,
     initialData: import('@vendure/core').InitialData,
     initialData: import('@vendure/core').InitialData,
     loggingFn?: (message: string) => void,
     loggingFn?: (message: string) => void,
 ) {
 ) {
@@ -84,7 +84,7 @@ export async function populateCollections(
 }
 }
 
 
 export async function importProductsFromCsv(
 export async function importProductsFromCsv(
-    app: INestApplication,
+    app: INestApplicationContext,
     productsCsvPath: string,
     productsCsvPath: string,
     languageCode: import('@vendure/core').LanguageCode,
     languageCode: import('@vendure/core').LanguageCode,
 ): Promise<import('@vendure/core').ImportProgress> {
 ): Promise<import('@vendure/core').ImportProgress> {

+ 3 - 8
packages/core/src/common/types/injectable-strategy.ts

@@ -13,18 +13,13 @@ export interface InjectableStrategy {
      * Defines setup logic to be run during application bootstrap. Receives
      * Defines setup logic to be run during application bootstrap. Receives
      * the {@link Injector} as an argument, which allows application providers
      * the {@link Injector} as an argument, which allows application providers
      * to be used as part of the setup. This hook will be called on both the
      * to be used as part of the setup. This hook will be called on both the
-     * main server and the worker processes. If you have code which should only
-     * run on either the server on the worker, then inject the {@link ProcessContext}
-     * to check the current context.
+     * main server and the worker processes.
      *
      *
      * @example
      * @example
      * ```TypeScript
      * ```TypeScript
      * async init(injector: Injector) {
      * async init(injector: Injector) {
-     *   const processContext = injector.get(ProcessContext);
-     *   if (processContext.isServer) {
-     *     const myService = injector.get(MyService);
-     *     await myService.doSomething();
-     *   }
+     *   const myService = injector.get(MyService);
+     *   await myService.doSomething();
      * }
      * }
      * ```
      * ```
      */
      */

+ 20 - 20
packages/core/src/config/catalog/default-product-variant-price-calculation-strategy.spec.ts

@@ -29,9 +29,9 @@ describe('DefaultProductVariantPriceCalculationStrategy', () => {
     });
     });
 
 
     describe('with prices which do not include tax', () => {
     describe('with prices which do not include tax', () => {
-        it('standard tax, default zone', () => {
+        it('standard tax, default zone', async () => {
             const ctx = createRequestContext({ pricesIncludeTax: false });
             const ctx = createRequestContext({ pricesIncludeTax: false });
-            const result = strategy.calculate({
+            const result = await strategy.calculate({
                 inputPrice,
                 inputPrice,
                 taxCategory: taxCategoryStandard,
                 taxCategory: taxCategoryStandard,
                 activeTaxZone: zoneDefault,
                 activeTaxZone: zoneDefault,
@@ -44,9 +44,9 @@ describe('DefaultProductVariantPriceCalculationStrategy', () => {
             });
             });
         });
         });
 
 
-        it('reduced tax, default zone', () => {
+        it('reduced tax, default zone', async () => {
             const ctx = createRequestContext({ pricesIncludeTax: false });
             const ctx = createRequestContext({ pricesIncludeTax: false });
-            const result = strategy.calculate({
+            const result = await strategy.calculate({
                 inputPrice,
                 inputPrice,
                 taxCategory: taxCategoryReduced,
                 taxCategory: taxCategoryReduced,
                 activeTaxZone: zoneDefault,
                 activeTaxZone: zoneDefault,
@@ -59,9 +59,9 @@ describe('DefaultProductVariantPriceCalculationStrategy', () => {
             });
             });
         });
         });
 
 
-        it('standard tax, other zone', () => {
+        it('standard tax, other zone', async () => {
             const ctx = createRequestContext({ pricesIncludeTax: false });
             const ctx = createRequestContext({ pricesIncludeTax: false });
-            const result = strategy.calculate({
+            const result = await strategy.calculate({
                 inputPrice,
                 inputPrice,
                 taxCategory: taxCategoryStandard,
                 taxCategory: taxCategoryStandard,
                 activeTaxZone: zoneOther,
                 activeTaxZone: zoneOther,
@@ -74,9 +74,9 @@ describe('DefaultProductVariantPriceCalculationStrategy', () => {
             });
             });
         });
         });
 
 
-        it('reduced tax, other zone', () => {
+        it('reduced tax, other zone', async () => {
             const ctx = createRequestContext({ pricesIncludeTax: false });
             const ctx = createRequestContext({ pricesIncludeTax: false });
-            const result = strategy.calculate({
+            const result = await strategy.calculate({
                 inputPrice,
                 inputPrice,
                 taxCategory: taxCategoryReduced,
                 taxCategory: taxCategoryReduced,
                 activeTaxZone: zoneOther,
                 activeTaxZone: zoneOther,
@@ -89,9 +89,9 @@ describe('DefaultProductVariantPriceCalculationStrategy', () => {
             });
             });
         });
         });
 
 
-        it('standard tax, unconfigured zone', () => {
+        it('standard tax, unconfigured zone', async () => {
             const ctx = createRequestContext({ pricesIncludeTax: false });
             const ctx = createRequestContext({ pricesIncludeTax: false });
-            const result = strategy.calculate({
+            const result = await strategy.calculate({
                 inputPrice,
                 inputPrice,
                 taxCategory: taxCategoryReduced,
                 taxCategory: taxCategoryReduced,
                 activeTaxZone: zoneWithNoTaxRate,
                 activeTaxZone: zoneWithNoTaxRate,
@@ -106,9 +106,9 @@ describe('DefaultProductVariantPriceCalculationStrategy', () => {
     });
     });
 
 
     describe('with prices which include tax', () => {
     describe('with prices which include tax', () => {
-        it('standard tax, default zone', () => {
+        it('standard tax, default zone', async () => {
             const ctx = createRequestContext({ pricesIncludeTax: true });
             const ctx = createRequestContext({ pricesIncludeTax: true });
-            const result = strategy.calculate({
+            const result = await strategy.calculate({
                 inputPrice,
                 inputPrice,
                 taxCategory: taxCategoryStandard,
                 taxCategory: taxCategoryStandard,
                 activeTaxZone: zoneDefault,
                 activeTaxZone: zoneDefault,
@@ -121,9 +121,9 @@ describe('DefaultProductVariantPriceCalculationStrategy', () => {
             });
             });
         });
         });
 
 
-        it('reduced tax, default zone', () => {
+        it('reduced tax, default zone', async () => {
             const ctx = createRequestContext({ pricesIncludeTax: true });
             const ctx = createRequestContext({ pricesIncludeTax: true });
-            const result = strategy.calculate({
+            const result = await strategy.calculate({
                 inputPrice,
                 inputPrice,
                 taxCategory: taxCategoryReduced,
                 taxCategory: taxCategoryReduced,
                 activeTaxZone: zoneDefault,
                 activeTaxZone: zoneDefault,
@@ -136,9 +136,9 @@ describe('DefaultProductVariantPriceCalculationStrategy', () => {
             });
             });
         });
         });
 
 
-        it('standard tax, other zone', () => {
+        it('standard tax, other zone', async () => {
             const ctx = createRequestContext({ pricesIncludeTax: true });
             const ctx = createRequestContext({ pricesIncludeTax: true });
-            const result = strategy.calculate({
+            const result = await strategy.calculate({
                 inputPrice,
                 inputPrice,
                 taxCategory: taxCategoryStandard,
                 taxCategory: taxCategoryStandard,
                 activeTaxZone: zoneOther,
                 activeTaxZone: zoneOther,
@@ -151,9 +151,9 @@ describe('DefaultProductVariantPriceCalculationStrategy', () => {
             });
             });
         });
         });
 
 
-        it('reduced tax, other zone', () => {
+        it('reduced tax, other zone', async () => {
             const ctx = createRequestContext({ pricesIncludeTax: true });
             const ctx = createRequestContext({ pricesIncludeTax: true });
-            const result = strategy.calculate({
+            const result = await strategy.calculate({
                 inputPrice,
                 inputPrice,
                 taxCategory: taxCategoryReduced,
                 taxCategory: taxCategoryReduced,
                 activeTaxZone: zoneOther,
                 activeTaxZone: zoneOther,
@@ -166,9 +166,9 @@ describe('DefaultProductVariantPriceCalculationStrategy', () => {
             });
             });
         });
         });
 
 
-        it('standard tax, unconfigured zone', () => {
+        it('standard tax, unconfigured zone', async () => {
             const ctx = createRequestContext({ pricesIncludeTax: true });
             const ctx = createRequestContext({ pricesIncludeTax: true });
-            const result = strategy.calculate({
+            const result = await strategy.calculate({
                 inputPrice,
                 inputPrice,
                 taxCategory: taxCategoryStandard,
                 taxCategory: taxCategoryStandard,
                 activeTaxZone: zoneWithNoTaxRate,
                 activeTaxZone: zoneWithNoTaxRate,

+ 4 - 3
packages/core/src/config/catalog/default-product-variant-price-calculation-strategy.ts

@@ -21,18 +21,19 @@ export class DefaultProductVariantPriceCalculationStrategy implements ProductVar
         this.taxRateService = injector.get(TaxRateService);
         this.taxRateService = injector.get(TaxRateService);
     }
     }
 
 
-    calculate(args: ProductVariantPriceCalculationArgs): PriceCalculationResult {
+    async calculate(args: ProductVariantPriceCalculationArgs): Promise<PriceCalculationResult> {
         const { inputPrice, activeTaxZone, ctx, taxCategory } = args;
         const { inputPrice, activeTaxZone, ctx, taxCategory } = args;
         let price = inputPrice;
         let price = inputPrice;
         let priceIncludesTax = false;
         let priceIncludesTax = false;
-        const taxRate = this.taxRateService.getApplicableTaxRate(activeTaxZone, taxCategory);
+        const taxRate = await this.taxRateService.getApplicableTaxRate(ctx, activeTaxZone, taxCategory);
 
 
         if (ctx.channel.pricesIncludeTax) {
         if (ctx.channel.pricesIncludeTax) {
             const isDefaultZone = idsAreEqual(activeTaxZone.id, ctx.channel.defaultTaxZone.id);
             const isDefaultZone = idsAreEqual(activeTaxZone.id, ctx.channel.defaultTaxZone.id);
             if (isDefaultZone) {
             if (isDefaultZone) {
                 priceIncludesTax = true;
                 priceIncludesTax = true;
             } else {
             } else {
-                const taxRateForDefaultZone = this.taxRateService.getApplicableTaxRate(
+                const taxRateForDefaultZone = await this.taxRateService.getApplicableTaxRate(
+                    ctx,
                     ctx.channel.defaultTaxZone,
                     ctx.channel.defaultTaxZone,
                     taxCategory,
                     taxCategory,
                 );
                 );

+ 1 - 1
packages/core/src/config/catalog/product-variant-price-calculation-strategy.ts

@@ -12,7 +12,7 @@ import { TaxRateService } from '../../service/services/tax-rate.service';
  * @docsPage ProductVariantPriceCalculationStrategy
  * @docsPage ProductVariantPriceCalculationStrategy
  */
  */
 export interface ProductVariantPriceCalculationStrategy extends InjectableStrategy {
 export interface ProductVariantPriceCalculationStrategy extends InjectableStrategy {
-    calculate(args: ProductVariantPriceCalculationArgs): PriceCalculationResult;
+    calculate(args: ProductVariantPriceCalculationArgs): Promise<PriceCalculationResult>;
 }
 }
 
 
 /**
 /**

+ 5 - 25
packages/core/src/config/config.module.ts

@@ -4,7 +4,6 @@ import { ModuleRef } from '@nestjs/core';
 import { ConfigurableOperationDef } from '../common/configurable-operation';
 import { ConfigurableOperationDef } from '../common/configurable-operation';
 import { Injector } from '../common/injector';
 import { Injector } from '../common/injector';
 import { InjectableStrategy } from '../common/types/injectable-strategy';
 import { InjectableStrategy } from '../common/types/injectable-strategy';
-import { ProcessContext } from '../process-context/process-context';
 
 
 import { ConfigService } from './config.service';
 import { ConfigService } from './config.service';
 
 
@@ -13,35 +12,16 @@ import { ConfigService } from './config.service';
     exports: [ConfigService],
     exports: [ConfigService],
 })
 })
 export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdown {
 export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdown {
-    constructor(
-        private configService: ConfigService,
-        private moduleRef: ModuleRef,
-        private processContext: ProcessContext,
-    ) {}
+    constructor(private configService: ConfigService, private moduleRef: ModuleRef) {}
 
 
     async onApplicationBootstrap() {
     async onApplicationBootstrap() {
-        if (this.runInjectableStrategyLifecycleHooks()) {
-            await this.initInjectableStrategies();
-            await this.initConfigurableOperations();
-        }
+        await this.initInjectableStrategies();
+        await this.initConfigurableOperations();
     }
     }
 
 
     async onApplicationShutdown(signal?: string) {
     async onApplicationShutdown(signal?: string) {
-        if (this.runInjectableStrategyLifecycleHooks()) {
-            await this.destroyInjectableStrategies();
-            await this.destroyConfigurableOperations();
-        }
-    }
-
-    /**
-     * The lifecycle hooks of the configured strategies should be run if we are on the main
-     * server process _or_ if we are on the worker running independently of the main process.
-     */
-    private runInjectableStrategyLifecycleHooks(): boolean {
-        return (
-            this.processContext.isServer ||
-            (this.processContext.isWorker && !this.configService.workerOptions.runInMainProcess)
-        );
+        await this.destroyInjectableStrategies();
+        await this.destroyConfigurableOperations();
     }
     }
 
 
     private async initInjectableStrategies() {
     private async initInjectableStrategies() {

+ 0 - 1
packages/core/src/config/config.service.mock.ts

@@ -41,7 +41,6 @@ export class MockConfigService implements MockClass<ConfigService> {
     emailOptions: {};
     emailOptions: {};
     importExportOptions: {};
     importExportOptions: {};
     orderOptions = {};
     orderOptions = {};
-    workerOptions = {};
     customFields = {};
     customFields = {};
 
 
     plugins = [];
     plugins = [];

+ 0 - 9
packages/core/src/config/config.service.ts

@@ -1,8 +1,5 @@
 import { DynamicModule, Injectable, Type } from '@nestjs/common';
 import { DynamicModule, Injectable, Type } from '@nestjs/common';
-import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface';
 import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { LanguageCode } from '@vendure/common/lib/generated-types';
-import { PluginDefinition } from 'apollo-server-core';
-import { RequestHandler } from 'express';
 import { ConnectionOptions } from 'typeorm';
 import { ConnectionOptions } from 'typeorm';
 
 
 import { getConfig } from './config-helpers';
 import { getConfig } from './config-helpers';
@@ -21,10 +18,8 @@ import {
     PromotionOptions,
     PromotionOptions,
     RuntimeVendureConfig,
     RuntimeVendureConfig,
     ShippingOptions,
     ShippingOptions,
-    SuperadminCredentials,
     TaxOptions,
     TaxOptions,
     VendureConfig,
     VendureConfig,
-    WorkerOptions,
 } from './vendure-config';
 } from './vendure-config';
 
 
 @Injectable()
 @Injectable()
@@ -107,10 +102,6 @@ export class ConfigService implements VendureConfig {
         return this.activeConfig.logger;
         return this.activeConfig.logger;
     }
     }
 
 
-    get workerOptions(): WorkerOptions {
-        return this.activeConfig.workerOptions;
-    }
-
     get jobQueueOptions(): Required<JobQueueOptions> {
     get jobQueueOptions(): Required<JobQueueOptions> {
         return this.activeConfig.jobQueueOptions;
         return this.activeConfig.jobQueueOptions;
     }
     }

+ 0 - 7
packages/core/src/config/default-config.ts

@@ -133,13 +133,6 @@ export const defaultConfig: RuntimeVendureConfig = {
     importExportOptions: {
     importExportOptions: {
         importAssetsDir: __dirname,
         importAssetsDir: __dirname,
     },
     },
-    workerOptions: {
-        runInMainProcess: false,
-        transport: Transport.TCP,
-        options: {
-            port: 3020,
-        },
-    },
     jobQueueOptions: {
     jobQueueOptions: {
         jobQueueStrategy: new InMemoryJobQueueStrategy(),
         jobQueueStrategy: new InMemoryJobQueueStrategy(),
         activeQueues: [],
         activeQueues: [],

+ 1 - 1
packages/core/src/config/job-queue/job-queue-strategy.ts

@@ -27,7 +27,7 @@ export interface JobQueueStrategy extends InjectableStrategy {
     start<Data extends JobData<Data> = {}>(
     start<Data extends JobData<Data> = {}>(
         queueName: string,
         queueName: string,
         process: (job: Job<Data>) => Promise<any>,
         process: (job: Job<Data>) => Promise<any>,
-    ): void;
+    ): Promise<void>;
 
 
     /**
     /**
      * @description
      * @description

+ 0 - 60
packages/core/src/config/vendure-config.ts

@@ -685,60 +685,6 @@ export interface ImportExportOptions {
     importAssetsDir?: string;
     importAssetsDir?: string;
 }
 }
 
 
-/**
- * @description
- * Options related to the Vendure Worker.
- *
- * @example
- * ```TypeScript
- * import { Transport } from '\@nestjs/microservices';
- *
- * const config: VendureConfig = {
- *     // ...
- *     workerOptions: {
- *         transport: Transport.TCP,
- *         options: {
- *             host: 'localhost',
- *             port: 3001,
- *         },
- *     },
- * }
- * ```
- *
- * @docsCategory worker
- */
-export interface WorkerOptions {
-    /**
-     * @description
-     * If set to `true`, the Worker will run be bootstrapped as part of the main Vendure server (when invoking the
-     * `bootstrap()` function) and will run in the same process. This mode is intended only for development and
-     * testing purposes, not for production, since running the Worker in the main process negates the benefits
-     * of having long-running or expensive tasks run in the background.
-     *
-     * @default false
-     */
-    runInMainProcess?: boolean;
-    /**
-     * @description
-     * Sets the transport protocol used to communicate with the Worker. Options include TCP, Redis, gPRC and more. See the
-     * [NestJS microservices documentation](https://docs.nestjs.com/microservices/basics) for a full list.
-     *
-     * @default Transport.TCP
-     */
-    transport?: Transport;
-    /**
-     * @description
-     * Additional options related to the chosen transport method. See See the
-     * [NestJS microservices documentation](https://docs.nestjs.com/microservices/basics) for details on the options relating to each of the
-     * transport methods.
-     *
-     * By default, the options for the TCP transport will run with the following settings:
-     * * host: 'localhost'
-     * * port: 3020
-     */
-    options?: ClientOptions['options'];
-}
-
 /**
 /**
  * @description
  * @description
  * Options related to the built-in job queue.
  * Options related to the built-in job queue.
@@ -876,11 +822,6 @@ export interface VendureConfig {
      * Configures how taxes are calculated on products.
      * Configures how taxes are calculated on products.
      */
      */
     taxOptions?: TaxOptions;
     taxOptions?: TaxOptions;
-    /**
-     * @description
-     * Configures the Vendure Worker, which is used for long-running background tasks.
-     */
-    workerOptions?: WorkerOptions;
     /**
     /**
      * @description
      * @description
      * Configures how the job queue is persisted and processed.
      * Configures how the job queue is persisted and processed.
@@ -905,7 +846,6 @@ export interface RuntimeVendureConfig extends Required<VendureConfig> {
     orderOptions: Required<OrderOptions>;
     orderOptions: Required<OrderOptions>;
     promotionOptions: Required<PromotionOptions>;
     promotionOptions: Required<PromotionOptions>;
     shippingOptions: Required<ShippingOptions>;
     shippingOptions: Required<ShippingOptions>;
-    workerOptions: Required<WorkerOptions>;
     taxOptions: Required<TaxOptions>;
     taxOptions: Required<TaxOptions>;
 }
 }
 
 

+ 1 - 2
packages/core/src/index.ts

@@ -7,11 +7,10 @@ export * from './event-bus/index';
 export * from './health-check/index';
 export * from './health-check/index';
 export * from './job-queue/index';
 export * from './job-queue/index';
 export * from './plugin/index';
 export * from './plugin/index';
-export * from './process-context/index';
 export * from './entity/index';
 export * from './entity/index';
 export * from './data-import/index';
 export * from './data-import/index';
 export * from './service/index';
 export * from './service/index';
-export * from './worker/index';
+export * from './async/index';
 export * from '@vendure/common/lib/shared-types';
 export * from '@vendure/common/lib/shared-types';
 export {
 export {
     Permission,
     Permission,

+ 1 - 0
packages/core/src/job-queue/constants.ts

@@ -0,0 +1 @@
+export const loggerCtx = 'JobQueue';

+ 15 - 19
packages/core/src/job-queue/job-queue.service.spec.ts

@@ -8,7 +8,6 @@ import { take } from 'rxjs/operators';
 
 
 import { Injector } from '../common';
 import { Injector } from '../common';
 import { ConfigService } from '../config/config.service';
 import { ConfigService } from '../config/config.service';
-import { ProcessContext, WorkerProcessContext } from '../process-context/process-context';
 
 
 import { Job } from './job';
 import { Job } from './job';
 import { JobQueueService } from './job-queue.service';
 import { JobQueueService } from './job-queue.service';
@@ -22,15 +21,12 @@ describe('JobQueueService', () => {
 
 
     beforeEach(async () => {
     beforeEach(async () => {
         module = await Test.createTestingModule({
         module = await Test.createTestingModule({
-            providers: [
-                { provide: ConfigService, useClass: MockConfigService },
-                { provide: ProcessContext, useClass: WorkerProcessContext },
-                JobQueueService,
-            ],
+            providers: [{ provide: ConfigService, useClass: MockConfigService }, JobQueueService],
         }).compile();
         }).compile();
+        await module.init();
 
 
         jobQueueService = module.get(JobQueueService);
         jobQueueService = module.get(JobQueueService);
-        await module.init();
+        await jobQueueService.start();
     });
     });
 
 
     afterEach(async () => {
     afterEach(async () => {
@@ -40,7 +36,7 @@ describe('JobQueueService', () => {
     it('data is passed into job', async () => {
     it('data is passed into job', async () => {
         const subject = new Subject<string>();
         const subject = new Subject<string>();
         const subNext = subject.pipe(take(1)).toPromise();
         const subNext = subject.pipe(take(1)).toPromise();
-        const testQueue = jobQueueService.createQueue<string>({
+        const testQueue = await jobQueueService.createQueue<string>({
             name: 'test',
             name: 'test',
             process: async job => {
             process: async job => {
                 subject.next(job.data);
                 subject.next(job.data);
@@ -54,7 +50,7 @@ describe('JobQueueService', () => {
 
 
     it('job marked as complete', async () => {
     it('job marked as complete', async () => {
         const subject = new Subject<string>();
         const subject = new Subject<string>();
-        const testQueue = jobQueueService.createQueue<string>({
+        const testQueue = await jobQueueService.createQueue<string>({
             name: 'test',
             name: 'test',
             process: job => {
             process: job => {
                 return subject.toPromise();
                 return subject.toPromise();
@@ -78,7 +74,7 @@ describe('JobQueueService', () => {
 
 
     it('job marked as failed when exception thrown', async () => {
     it('job marked as failed when exception thrown', async () => {
         const subject = new Subject();
         const subject = new Subject();
-        const testQueue = jobQueueService.createQueue<string>({
+        const testQueue = await jobQueueService.createQueue<string>({
             name: 'test',
             name: 'test',
             process: async job => {
             process: async job => {
                 const result = await subject.toPromise();
                 const result = await subject.toPromise();
@@ -102,7 +98,7 @@ describe('JobQueueService', () => {
 
 
     it('job marked as failed when async error thrown', async () => {
     it('job marked as failed when async error thrown', async () => {
         const err = new Error('something bad happened');
         const err = new Error('something bad happened');
-        const testQueue = jobQueueService.createQueue<string>({
+        const testQueue = await jobQueueService.createQueue<string>({
             name: 'test',
             name: 'test',
             process: async job => {
             process: async job => {
                 throw err;
                 throw err;
@@ -119,7 +115,7 @@ describe('JobQueueService', () => {
 
 
     it('jobs processed in FIFO queue', async () => {
     it('jobs processed in FIFO queue', async () => {
         const subject = new Subject();
         const subject = new Subject();
-        const testQueue = jobQueueService.createQueue<string>({
+        const testQueue = await jobQueueService.createQueue<string>({
             name: 'test',
             name: 'test',
             process: job => {
             process: job => {
                 return subject.pipe(take(1)).toPromise();
                 return subject.pipe(take(1)).toPromise();
@@ -164,7 +160,7 @@ describe('JobQueueService', () => {
         testingJobQueueStrategy.concurrency = 2;
         testingJobQueueStrategy.concurrency = 2;
 
 
         const subject = new Subject();
         const subject = new Subject();
-        const testQueue = jobQueueService.createQueue<string>({
+        const testQueue = await jobQueueService.createQueue<string>({
             name: 'test',
             name: 'test',
             process: job => {
             process: job => {
                 return subject.pipe(take(1)).toPromise();
                 return subject.pipe(take(1)).toPromise();
@@ -212,7 +208,7 @@ describe('JobQueueService', () => {
             }),
             }),
         ]);
         ]);
 
 
-        const testQueue = jobQueueService.createQueue<string>({
+        const testQueue = await jobQueueService.createQueue<string>({
             name: 'test',
             name: 'test',
             process: async job => {
             process: async job => {
                 return;
                 return;
@@ -232,7 +228,7 @@ describe('JobQueueService', () => {
 
 
     it('retries', async () => {
     it('retries', async () => {
         const subject = new Subject<boolean>();
         const subject = new Subject<boolean>();
-        const testQueue = jobQueueService.createQueue<string>({
+        const testQueue = await jobQueueService.createQueue<string>({
             name: 'test',
             name: 'test',
             process: job => {
             process: job => {
                 return subject
                 return subject
@@ -275,7 +271,7 @@ describe('JobQueueService', () => {
             .jobQueueStrategy as TestingJobQueueStrategy;
             .jobQueueStrategy as TestingJobQueueStrategy;
 
 
         const subject = new Subject<boolean>();
         const subject = new Subject<boolean>();
-        const testQueue = jobQueueService.createQueue<string>({
+        const testQueue = await jobQueueService.createQueue<string>({
             name: 'test',
             name: 'test',
             process: job => {
             process: job => {
                 return subject.pipe(take(1)).toPromise();
                 return subject.pipe(take(1)).toPromise();
@@ -288,7 +284,7 @@ describe('JobQueueService', () => {
 
 
         expect((await testingJobQueueStrategy.findOne(testJob.id!))?.state).toBe(JobState.RUNNING);
         expect((await testingJobQueueStrategy.findOne(testJob.id!))?.state).toBe(JobState.RUNNING);
 
 
-        await testQueue.destroy();
+        await testQueue.stop();
 
 
         expect((await testingJobQueueStrategy.findOne(testJob.id!))?.state).toBe(JobState.PENDING);
         expect((await testingJobQueueStrategy.findOne(testJob.id!))?.state).toBe(JobState.PENDING);
     }, 10000);
     }, 10000);
@@ -297,7 +293,7 @@ describe('JobQueueService', () => {
         module.get(ConfigService).jobQueueOptions.activeQueues = ['test'];
         module.get(ConfigService).jobQueueOptions.activeQueues = ['test'];
 
 
         const subject = new Subject();
         const subject = new Subject();
-        const testQueue = jobQueueService.createQueue<string>({
+        const testQueue = await jobQueueService.createQueue<string>({
             name: 'test',
             name: 'test',
             process: job => {
             process: job => {
                 return subject.toPromise();
                 return subject.toPromise();
@@ -322,7 +318,7 @@ describe('JobQueueService', () => {
         module.get(ConfigService).jobQueueOptions.activeQueues = ['another'];
         module.get(ConfigService).jobQueueOptions.activeQueues = ['another'];
 
 
         const subject = new Subject();
         const subject = new Subject();
-        const testQueue = jobQueueService.createQueue<string>({
+        const testQueue = await jobQueueService.createQueue<string>({
             name: 'test',
             name: 'test',
             process: job => {
             process: job => {
                 return subject.toPromise();
                 return subject.toPromise();

+ 24 - 27
packages/core/src/job-queue/job-queue.service.ts

@@ -1,9 +1,9 @@
-import { Injectable, OnApplicationBootstrap, OnModuleDestroy } from '@nestjs/common';
+import { Injectable, OnModuleDestroy } from '@nestjs/common';
 import { JobQueue as GraphQlJobQueue } from '@vendure/common/lib/generated-types';
 import { JobQueue as GraphQlJobQueue } from '@vendure/common/lib/generated-types';
 
 
-import { ConfigService, JobQueueStrategy } from '../config';
-import { ProcessContext } from '../process-context';
+import { ConfigService, JobQueueStrategy, Logger } from '../config';
 
 
+import { loggerCtx } from './constants';
 import { JobQueue } from './job-queue';
 import { JobQueue } from './job-queue';
 import { CreateQueueOptions, JobData } from './types';
 import { CreateQueueOptions, JobData } from './types';
 
 
@@ -43,46 +43,47 @@ import { CreateQueueOptions, JobData } from './types';
  * @docsCategory JobQueue
  * @docsCategory JobQueue
  */
  */
 @Injectable()
 @Injectable()
-export class JobQueueService implements OnApplicationBootstrap, OnModuleDestroy {
+export class JobQueueService implements OnModuleDestroy {
     private queues: Array<JobQueue<any>> = [];
     private queues: Array<JobQueue<any>> = [];
-    private hasInitialized = false;
+    private hasStarted = false;
 
 
     private get jobQueueStrategy(): JobQueueStrategy {
     private get jobQueueStrategy(): JobQueueStrategy {
         return this.configService.jobQueueOptions.jobQueueStrategy;
         return this.configService.jobQueueOptions.jobQueueStrategy;
     }
     }
 
 
-    constructor(private configService: ConfigService, private processContext: ProcessContext) {}
-
-    /** @internal */
-    async onApplicationBootstrap() {
-        this.hasInitialized = true;
-        for (const queue of this.queues) {
-            if (!queue.started && this.shouldStartQueue(queue.name)) {
-                queue.start();
-            }
-        }
-    }
+    constructor(private configService: ConfigService) {}
 
 
     /** @internal */
     /** @internal */
     onModuleDestroy() {
     onModuleDestroy() {
-        this.hasInitialized = false;
-        return Promise.all(this.queues.map(q => q.destroy()));
+        this.hasStarted = false;
+        return Promise.all(this.queues.map(q => q.stop()));
     }
     }
 
 
     /**
     /**
      * @description
      * @description
      * Configures and creates a new {@link JobQueue} instance.
      * Configures and creates a new {@link JobQueue} instance.
      */
      */
-    createQueue<Data extends JobData<Data>>(options: CreateQueueOptions<Data>): JobQueue<Data> {
-        const { jobQueueStrategy } = this.configService.jobQueueOptions;
-        const queue = new JobQueue(options, jobQueueStrategy);
-        if (this.hasInitialized && this.shouldStartQueue(queue.name)) {
-            queue.start();
+    async createQueue<Data extends JobData<Data>>(
+        options: CreateQueueOptions<Data>,
+    ): Promise<JobQueue<Data>> {
+        const queue = new JobQueue(options, this.jobQueueStrategy);
+        if (this.hasStarted && this.shouldStartQueue(queue.name)) {
+            await queue.start();
         }
         }
         this.queues.push(queue);
         this.queues.push(queue);
         return queue;
         return queue;
     }
     }
 
 
+    async start(): Promise<void> {
+        this.hasStarted = true;
+        for (const queue of this.queues) {
+            if (!queue.started && this.shouldStartQueue(queue.name)) {
+                Logger.info(`Starting queue: ${queue.name}`, loggerCtx);
+                await queue.start();
+            }
+        }
+    }
+
     /**
     /**
      * @description
      * @description
      * Returns an array of `{ name: string; running: boolean; }` for each
      * Returns an array of `{ name: string; running: boolean; }` for each
@@ -96,10 +97,6 @@ export class JobQueueService implements OnApplicationBootstrap, OnModuleDestroy
     }
     }
 
 
     private shouldStartQueue(queueName: string): boolean {
     private shouldStartQueue(queueName: string): boolean {
-        if (this.processContext.isServer) {
-            return false;
-        }
-
         if (this.configService.jobQueueOptions.activeQueues.length > 0) {
         if (this.configService.jobQueueOptions.activeQueues.length > 0) {
             if (!this.configService.jobQueueOptions.activeQueues.includes(queueName)) {
             if (!this.configService.jobQueueOptions.activeQueues.includes(queueName)) {
                 return false;
                 return false;

+ 3 - 13
packages/core/src/job-queue/job-queue.ts

@@ -34,26 +34,16 @@ export class JobQueue<Data extends JobData<Data> = {}> {
     constructor(private options: CreateQueueOptions<Data>, private jobQueueStrategy: JobQueueStrategy) {}
     constructor(private options: CreateQueueOptions<Data>, private jobQueueStrategy: JobQueueStrategy) {}
 
 
     /** @internal */
     /** @internal */
-    start() {
+    async start() {
         if (this.running) {
         if (this.running) {
             return;
             return;
         }
         }
         this.running = true;
         this.running = true;
-        this.jobQueueStrategy.start<Data>(this.options.name, this.options.process);
+        await this.jobQueueStrategy.start<Data>(this.options.name, this.options.process);
     }
     }
 
 
     /** @internal */
     /** @internal */
-    pause() {
-        Logger.debug(`Pausing JobQueue "${this.options.name}"`);
-        if (!this.running) {
-            return;
-        }
-        this.running = false;
-        this.jobQueueStrategy.stop(this.options.name, this.options.process);
-    }
-
-    /** @internal */
-    async destroy(): Promise<void> {
+    async stop(): Promise<void> {
         if (!this.running) {
         if (!this.running) {
             return;
             return;
         }
         }

+ 2 - 2
packages/core/src/job-queue/polling-job-queue-strategy.ts

@@ -130,10 +130,10 @@ export abstract class PollingJobQueueStrategy extends InjectableJobQueueStrategy
         super();
         super();
     }
     }
 
 
-    start<Data extends JobData<Data> = {}>(
+    async start<Data extends JobData<Data> = {}>(
         queueName: string,
         queueName: string,
         process: (job: Job<Data>) => Promise<any>,
         process: (job: Job<Data>) => Promise<any>,
-    ): void {
+    ) {
         if (!this.hasInitialized) {
         if (!this.hasInitialized) {
             this.started.set(queueName, process);
             this.started.set(queueName, process);
             return;
             return;

+ 1 - 4
packages/core/src/plugin/default-search-plugin/default-search-plugin.ts

@@ -57,11 +57,10 @@ export interface DefaultSearchReindexResponse extends SearchReindexResponse {
  */
  */
 @VendurePlugin({
 @VendurePlugin({
     imports: [PluginCommonModule],
     imports: [PluginCommonModule],
-    providers: [FulltextSearchService, SearchIndexService],
+    providers: [FulltextSearchService, SearchIndexService, IndexerController],
     adminApiExtensions: { resolvers: [AdminFulltextSearchResolver] },
     adminApiExtensions: { resolvers: [AdminFulltextSearchResolver] },
     shopApiExtensions: { resolvers: [ShopFulltextSearchResolver] },
     shopApiExtensions: { resolvers: [ShopFulltextSearchResolver] },
     entities: [SearchIndexItem],
     entities: [SearchIndexItem],
-    workers: [IndexerController],
 })
 })
 export class DefaultSearchPlugin implements OnApplicationBootstrap {
 export class DefaultSearchPlugin implements OnApplicationBootstrap {
     /** @internal */
     /** @internal */
@@ -69,8 +68,6 @@ export class DefaultSearchPlugin implements OnApplicationBootstrap {
 
 
     /** @internal */
     /** @internal */
     async onApplicationBootstrap() {
     async onApplicationBootstrap() {
-        this.searchIndexService.initJobQueue();
-
         this.eventBus.ofType(ProductEvent).subscribe(event => {
         this.eventBus.ofType(ProductEvent).subscribe(event => {
             if (event.type === 'deleted') {
             if (event.type === 'deleted') {
                 return this.searchIndexService.deleteProduct(event.ctx, event.product);
                 return this.searchIndexService.deleteProduct(event.ctx, event.product);

+ 61 - 106
packages/core/src/plugin/default-search-plugin/indexer/indexer.controller.ts

@@ -1,5 +1,4 @@
-import { Controller } from '@nestjs/common';
-import { MessagePattern } from '@nestjs/microservices';
+import { Injectable } from '@nestjs/common';
 import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { ID } from '@vendure/common/lib/shared-types';
 import { ID } from '@vendure/common/lib/shared-types';
 import { unique } from '@vendure/common/lib/unique';
 import { unique } from '@vendure/common/lib/unique';
@@ -7,6 +6,7 @@ import { Observable } from 'rxjs';
 import { FindOptionsUtils } from 'typeorm/find-options/FindOptionsUtils';
 import { FindOptionsUtils } from 'typeorm/find-options/FindOptionsUtils';
 
 
 import { RequestContext } from '../../../api/common/request-context';
 import { RequestContext } from '../../../api/common/request-context';
+import { asyncObservable } from '../../../async';
 import { AsyncQueue } from '../../../common/async-queue';
 import { AsyncQueue } from '../../../common/async-queue';
 import { Translatable, Translation } from '../../../common/types/locale-types';
 import { Translatable, Translation } from '../../../common/types/locale-types';
 import { idsAreEqual } from '../../../common/utils';
 import { idsAreEqual } from '../../../common/utils';
@@ -17,21 +17,16 @@ import { ProductVariant } from '../../../entity/product-variant/product-variant.
 import { Product } from '../../../entity/product/product.entity';
 import { Product } from '../../../entity/product/product.entity';
 import { ProductVariantService } from '../../../service/services/product-variant.service';
 import { ProductVariantService } from '../../../service/services/product-variant.service';
 import { TransactionalConnection } from '../../../service/transaction/transactional-connection';
 import { TransactionalConnection } from '../../../service/transaction/transactional-connection';
-import { asyncObservable } from '../../../worker/async-observable';
 import { SearchIndexItem } from '../search-index-item.entity';
 import { SearchIndexItem } from '../search-index-item.entity';
 import {
 import {
-    AssignProductToChannelMessage,
-    AssignVariantToChannelMessage,
-    DeleteAssetMessage,
-    DeleteProductMessage,
-    DeleteVariantMessage,
-    ReindexMessage,
-    RemoveProductFromChannelMessage,
-    RemoveVariantFromChannelMessage,
-    UpdateAssetMessage,
-    UpdateProductMessage,
-    UpdateVariantMessage,
-    UpdateVariantsByIdMessage,
+    ProductChannelMessageData,
+    ReindexMessageData,
+    ReindexMessageResponse,
+    UpdateAssetMessageData,
+    UpdateProductMessageData,
+    UpdateVariantMessageData,
+    UpdateVariantsByIdMessageData,
+    VariantChannelMessageData,
 } from '../types';
 } from '../types';
 
 
 export const BATCH_SIZE = 1000;
 export const BATCH_SIZE = 1000;
@@ -52,7 +47,7 @@ export const variantRelations = [
 
 
 export const workerLoggerCtx = 'DefaultSearchPlugin Worker';
 export const workerLoggerCtx = 'DefaultSearchPlugin Worker';
 
 
-@Controller()
+@Injectable()
 export class IndexerController {
 export class IndexerController {
     private queue = new AsyncQueue('search-index');
     private queue = new AsyncQueue('search-index');
 
 
@@ -62,8 +57,7 @@ export class IndexerController {
         private configService: ConfigService,
         private configService: ConfigService,
     ) {}
     ) {}
 
 
-    @MessagePattern(ReindexMessage.pattern)
-    reindex({ ctx: rawContext }: ReindexMessage['data']): Observable<ReindexMessage['response']> {
+    reindex({ ctx: rawContext }: ReindexMessageData): Observable<ReindexMessageResponse> {
         const ctx = RequestContext.deserialize(rawContext);
         const ctx = RequestContext.deserialize(rawContext);
         return asyncObservable(async observer => {
         return asyncObservable(async observer => {
             const timeStart = Date.now();
             const timeStart = Date.now();
@@ -101,11 +95,10 @@ export class IndexerController {
         });
         });
     }
     }
 
 
-    @MessagePattern(UpdateVariantsByIdMessage.pattern)
     updateVariantsById({
     updateVariantsById({
         ctx: rawContext,
         ctx: rawContext,
         ids,
         ids,
-    }: UpdateVariantsByIdMessage['data']): Observable<UpdateVariantsByIdMessage['response']> {
+    }: UpdateVariantsByIdMessageData): Observable<ReindexMessageResponse> {
         const ctx = RequestContext.deserialize(rawContext);
         const ctx = RequestContext.deserialize(rawContext);
 
 
         return asyncObservable(async observer => {
         return asyncObservable(async observer => {
@@ -140,117 +133,79 @@ export class IndexerController {
         });
         });
     }
     }
 
 
-    @MessagePattern(UpdateProductMessage.pattern)
-    updateProduct(data: UpdateProductMessage['data']): Observable<UpdateProductMessage['response']> {
+    async updateProduct(data: UpdateProductMessageData): Promise<boolean> {
         const ctx = RequestContext.deserialize(data.ctx);
         const ctx = RequestContext.deserialize(data.ctx);
-        return asyncObservable(async () => {
-            return this.updateProductInChannel(ctx, data.productId, ctx.channelId);
-        });
+        return this.updateProductInChannel(ctx, data.productId, ctx.channelId);
     }
     }
 
 
-    @MessagePattern(UpdateVariantMessage.pattern)
-    updateVariants(data: UpdateVariantMessage['data']): Observable<UpdateVariantMessage['response']> {
+    async updateVariants(data: UpdateVariantMessageData): Promise<boolean> {
         const ctx = RequestContext.deserialize(data.ctx);
         const ctx = RequestContext.deserialize(data.ctx);
-        return asyncObservable(async () => {
-            return this.updateVariantsInChannel(ctx, data.variantIds, ctx.channelId);
-        });
+        return this.updateVariantsInChannel(ctx, data.variantIds, ctx.channelId);
     }
     }
 
 
-    @MessagePattern(DeleteProductMessage.pattern)
-    deleteProduct(data: DeleteProductMessage['data']): Observable<DeleteProductMessage['response']> {
+    async deleteProduct(data: UpdateProductMessageData): Promise<boolean> {
         const ctx = RequestContext.deserialize(data.ctx);
         const ctx = RequestContext.deserialize(data.ctx);
-        return asyncObservable(async () => {
-            return this.deleteProductInChannel(ctx, data.productId, ctx.channelId);
-        });
+        return this.deleteProductInChannel(ctx, data.productId, ctx.channelId);
     }
     }
 
 
-    @MessagePattern(DeleteVariantMessage.pattern)
-    deleteVariant(data: DeleteVariantMessage['data']): Observable<DeleteVariantMessage['response']> {
+    async deleteVariant(data: UpdateVariantMessageData): Promise<boolean> {
         const ctx = RequestContext.deserialize(data.ctx);
         const ctx = RequestContext.deserialize(data.ctx);
-        return asyncObservable(async () => {
-            const variants = await this.connection.getRepository(ProductVariant).findByIds(data.variantIds);
-            if (variants.length) {
-                await this.removeSearchIndexItems(
-                    ctx.languageCode,
-                    ctx.channelId,
-                    variants.map(v => v.id),
-                );
-            }
-            return true;
-        });
+        const variants = await this.connection.getRepository(ProductVariant).findByIds(data.variantIds);
+        if (variants.length) {
+            await this.removeSearchIndexItems(
+                ctx.languageCode,
+                ctx.channelId,
+                variants.map(v => v.id),
+            );
+        }
+        return true;
     }
     }
 
 
-    @MessagePattern(AssignProductToChannelMessage.pattern)
-    assignProductToChannel(
-        data: AssignProductToChannelMessage['data'],
-    ): Observable<AssignProductToChannelMessage['response']> {
+    async assignProductToChannel(data: ProductChannelMessageData): Promise<boolean> {
         const ctx = RequestContext.deserialize(data.ctx);
         const ctx = RequestContext.deserialize(data.ctx);
-        return asyncObservable(async () => {
-            return this.updateProductInChannel(ctx, data.productId, data.channelId);
-        });
+        return this.updateProductInChannel(ctx, data.productId, data.channelId);
     }
     }
 
 
-    @MessagePattern(RemoveProductFromChannelMessage.pattern)
-    removeProductFromChannel(
-        data: RemoveProductFromChannelMessage['data'],
-    ): Observable<RemoveProductFromChannelMessage['response']> {
+    async removeProductFromChannel(data: ProductChannelMessageData): Promise<boolean> {
         const ctx = RequestContext.deserialize(data.ctx);
         const ctx = RequestContext.deserialize(data.ctx);
-        return asyncObservable(async () => {
-            return this.deleteProductInChannel(ctx, data.productId, data.channelId);
-        });
+        return this.deleteProductInChannel(ctx, data.productId, data.channelId);
     }
     }
 
 
-    @MessagePattern(AssignVariantToChannelMessage.pattern)
-    assignVariantToChannel(
-        data: AssignVariantToChannelMessage['data'],
-    ): Observable<AssignProductToChannelMessage['response']> {
+    async assignVariantToChannel(data: VariantChannelMessageData): Promise<boolean> {
         const ctx = RequestContext.deserialize(data.ctx);
         const ctx = RequestContext.deserialize(data.ctx);
-        return asyncObservable(async () => {
-            return this.updateVariantsInChannel(ctx, [data.productVariantId], data.channelId);
-        });
+        return this.updateVariantsInChannel(ctx, [data.productVariantId], data.channelId);
     }
     }
 
 
-    @MessagePattern(RemoveVariantFromChannelMessage.pattern)
-    removeVariantFromChannel(
-        data: RemoveVariantFromChannelMessage['data'],
-    ): Observable<RemoveProductFromChannelMessage['response']> {
+    async removeVariantFromChannel(data: VariantChannelMessageData): Promise<boolean> {
         const ctx = RequestContext.deserialize(data.ctx);
         const ctx = RequestContext.deserialize(data.ctx);
-        return asyncObservable(async () => {
-            await this.removeSearchIndexItems(ctx.languageCode, data.channelId, [data.productVariantId]);
-            return true;
-        });
+        await this.removeSearchIndexItems(ctx.languageCode, data.channelId, [data.productVariantId]);
+        return true;
     }
     }
 
 
-    @MessagePattern(UpdateAssetMessage.pattern)
-    updateAsset(data: UpdateAssetMessage['data']): Observable<UpdateAssetMessage['response']> {
-        return asyncObservable(async () => {
-            const id = data.asset.id;
-            function getFocalPoint(point?: { x: number; y: number }) {
-                return point && point.x && point.y ? point : null;
-            }
-            const focalPoint = getFocalPoint(data.asset.focalPoint);
-            await this.connection
-                .getRepository(SearchIndexItem)
-                .update({ productAssetId: id }, { productPreviewFocalPoint: focalPoint });
-            await this.connection
-                .getRepository(SearchIndexItem)
-                .update({ productVariantAssetId: id }, { productVariantPreviewFocalPoint: focalPoint });
-            return true;
-        });
+    async updateAsset(data: UpdateAssetMessageData): Promise<boolean> {
+        const id = data.asset.id;
+        function getFocalPoint(point?: { x: number; y: number }) {
+            return point && point.x && point.y ? point : null;
+        }
+        const focalPoint = getFocalPoint(data.asset.focalPoint);
+        await this.connection
+            .getRepository(SearchIndexItem)
+            .update({ productAssetId: id }, { productPreviewFocalPoint: focalPoint });
+        await this.connection
+            .getRepository(SearchIndexItem)
+            .update({ productVariantAssetId: id }, { productVariantPreviewFocalPoint: focalPoint });
+        return true;
     }
     }
 
 
-    @MessagePattern(DeleteAssetMessage.pattern)
-    deleteAsset(data: DeleteAssetMessage['data']): Observable<DeleteAssetMessage['response']> {
-        return asyncObservable(async () => {
-            const id = data.asset.id;
-            await this.connection
-                .getRepository(SearchIndexItem)
-                .update({ productAssetId: id }, { productAssetId: null });
-            await this.connection
-                .getRepository(SearchIndexItem)
-                .update({ productVariantAssetId: id }, { productVariantAssetId: null });
-            return true;
-        });
+    async deleteAsset(data: UpdateAssetMessageData): Promise<boolean> {
+        const id = data.asset.id;
+        await this.connection
+            .getRepository(SearchIndexItem)
+            .update({ productAssetId: id }, { productAssetId: null });
+        await this.connection
+            .getRepository(SearchIndexItem)
+            .update({ productVariantAssetId: id }, { productVariantAssetId: null });
+        return true;
     }
     }
 
 
     private async updateProductInChannel(
     private async updateProductInChannel(
@@ -363,7 +318,7 @@ export class IndexerController {
                         isAuthorized: true,
                         isAuthorized: true,
                         session: {} as any,
                         session: {} as any,
                     });
                     });
-                    this.productVariantService.applyChannelPriceAndTax(variant, ctx);
+                    await this.productVariantService.applyChannelPriceAndTax(variant, ctx);
                     items.push(
                     items.push(
                         new SearchIndexItem({
                         new SearchIndexItem({
                             channelId: channel.id,
                             channelId: channel.id,

+ 40 - 69
packages/core/src/plugin/default-search-plugin/indexer/search-index.service.ts

@@ -1,6 +1,7 @@
-import { Injectable } from '@nestjs/common';
+import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
 import { ID } from '@vendure/common/lib/shared-types';
 import { ID } from '@vendure/common/lib/shared-types';
 import { assertNever } from '@vendure/common/lib/shared-utils';
 import { assertNever } from '@vendure/common/lib/shared-utils';
+import { Observable } from 'rxjs';
 
 
 import { RequestContext } from '../../../api/common/request-context';
 import { RequestContext } from '../../../api/common/request-context';
 import { Logger } from '../../../config/logger/vendure-logger';
 import { Logger } from '../../../config/logger/vendure-logger';
@@ -10,65 +11,50 @@ import { Product } from '../../../entity/product/product.entity';
 import { Job } from '../../../job-queue/job';
 import { Job } from '../../../job-queue/job';
 import { JobQueue } from '../../../job-queue/job-queue';
 import { JobQueue } from '../../../job-queue/job-queue';
 import { JobQueueService } from '../../../job-queue/job-queue.service';
 import { JobQueueService } from '../../../job-queue/job-queue.service';
-import { WorkerMessage } from '../../../worker/types';
-import { WorkerService } from '../../../worker/worker.service';
-import {
-    AssignProductToChannelMessage,
-    AssignVariantToChannelMessage,
-    DeleteAssetMessage,
-    DeleteProductMessage,
-    DeleteVariantMessage,
-    ReindexMessage,
-    ReindexMessageResponse,
-    RemoveProductFromChannelMessage,
-    RemoveVariantFromChannelMessage,
-    UpdateAssetMessage,
-    UpdateIndexQueueJobData,
-    UpdateProductMessage,
-    UpdateVariantMessage,
-    UpdateVariantsByIdMessage,
-} from '../types';
-
-let updateIndexQueue: JobQueue<UpdateIndexQueueJobData> | undefined;
+import { ReindexMessageResponse, UpdateIndexQueueJobData } from '../types';
+
+import { IndexerController } from './indexer.controller';
 
 
 /**
 /**
  * This service is responsible for messaging the {@link IndexerController} with search index updates.
  * This service is responsible for messaging the {@link IndexerController} with search index updates.
  */
  */
 @Injectable()
 @Injectable()
-export class SearchIndexService {
-    constructor(private workerService: WorkerService, private jobService: JobQueueService) {}
+export class SearchIndexService implements OnApplicationBootstrap {
+    private updateIndexQueue: JobQueue<UpdateIndexQueueJobData>;
+
+    constructor(private jobService: JobQueueService, private indexerController: IndexerController) {}
 
 
-    initJobQueue() {
-        updateIndexQueue = this.jobService.createQueue({
+    async onApplicationBootstrap() {
+        this.updateIndexQueue = await this.jobService.createQueue({
             name: 'update-search-index',
             name: 'update-search-index',
             process: job => {
             process: job => {
                 const data = job.data;
                 const data = job.data;
                 switch (data.type) {
                 switch (data.type) {
                     case 'reindex':
                     case 'reindex':
                         Logger.verbose(`sending ReindexMessage`);
                         Logger.verbose(`sending ReindexMessage`);
-                        return this.sendMessageWithProgress(job, new ReindexMessage(data));
+                        return this.jobWithProgress(job, this.indexerController.reindex(data));
                     case 'update-product':
                     case 'update-product':
-                        return this.sendMessage(job, new UpdateProductMessage(data));
+                        return this.indexerController.updateProduct(data);
                     case 'update-variants':
                     case 'update-variants':
-                        return this.sendMessage(job, new UpdateVariantMessage(data));
+                        return this.indexerController.updateVariants(data);
                     case 'delete-product':
                     case 'delete-product':
-                        return this.sendMessage(job, new DeleteProductMessage(data));
+                        return this.indexerController.deleteProduct(data);
                     case 'delete-variant':
                     case 'delete-variant':
-                        return this.sendMessage(job, new DeleteVariantMessage(data));
+                        return this.indexerController.deleteVariant(data);
                     case 'update-variants-by-id':
                     case 'update-variants-by-id':
-                        return this.sendMessageWithProgress(job, new UpdateVariantsByIdMessage(data));
+                        return this.jobWithProgress(job, this.indexerController.updateVariantsById(data));
                     case 'update-asset':
                     case 'update-asset':
-                        return this.sendMessage(job, new UpdateAssetMessage(data));
+                        return this.indexerController.updateAsset(data);
                     case 'delete-asset':
                     case 'delete-asset':
-                        return this.sendMessage(job, new DeleteAssetMessage(data));
+                        return this.indexerController.deleteAsset(data);
                     case 'assign-product-to-channel':
                     case 'assign-product-to-channel':
-                        return this.sendMessage(job, new AssignProductToChannelMessage(data));
+                        return this.indexerController.assignProductToChannel(data);
                     case 'remove-product-from-channel':
                     case 'remove-product-from-channel':
-                        return this.sendMessage(job, new RemoveProductFromChannelMessage(data));
+                        return this.indexerController.removeProductFromChannel(data);
                     case 'assign-variant-to-channel':
                     case 'assign-variant-to-channel':
-                        return this.sendMessage(job, new AssignVariantToChannelMessage(data));
+                        return this.indexerController.assignVariantToChannel(data);
                     case 'remove-variant-from-channel':
                     case 'remove-variant-from-channel':
-                        return this.sendMessage(job, new RemoveVariantFromChannelMessage(data));
+                        return this.indexerController.removeVariantFromChannel(data);
                     default:
                     default:
                         assertNever(data);
                         assertNever(data);
                         return Promise.resolve();
                         return Promise.resolve();
@@ -78,41 +64,41 @@ export class SearchIndexService {
     }
     }
 
 
     reindex(ctx: RequestContext) {
     reindex(ctx: RequestContext) {
-        return this.addJobToQueue({ type: 'reindex', ctx: ctx.serialize() });
+        return this.updateIndexQueue.add({ type: 'reindex', ctx: ctx.serialize() });
     }
     }
 
 
     updateProduct(ctx: RequestContext, product: Product) {
     updateProduct(ctx: RequestContext, product: Product) {
-        this.addJobToQueue({ type: 'update-product', ctx: ctx.serialize(), productId: product.id });
+        this.updateIndexQueue.add({ type: 'update-product', ctx: ctx.serialize(), productId: product.id });
     }
     }
 
 
     updateVariants(ctx: RequestContext, variants: ProductVariant[]) {
     updateVariants(ctx: RequestContext, variants: ProductVariant[]) {
         const variantIds = variants.map(v => v.id);
         const variantIds = variants.map(v => v.id);
-        this.addJobToQueue({ type: 'update-variants', ctx: ctx.serialize(), variantIds });
+        this.updateIndexQueue.add({ type: 'update-variants', ctx: ctx.serialize(), variantIds });
     }
     }
 
 
     deleteProduct(ctx: RequestContext, product: Product) {
     deleteProduct(ctx: RequestContext, product: Product) {
-        this.addJobToQueue({ type: 'delete-product', ctx: ctx.serialize(), productId: product.id });
+        this.updateIndexQueue.add({ type: 'delete-product', ctx: ctx.serialize(), productId: product.id });
     }
     }
 
 
     deleteVariant(ctx: RequestContext, variants: ProductVariant[]) {
     deleteVariant(ctx: RequestContext, variants: ProductVariant[]) {
         const variantIds = variants.map(v => v.id);
         const variantIds = variants.map(v => v.id);
-        this.addJobToQueue({ type: 'delete-variant', ctx: ctx.serialize(), variantIds });
+        this.updateIndexQueue.add({ type: 'delete-variant', ctx: ctx.serialize(), variantIds });
     }
     }
 
 
     updateVariantsById(ctx: RequestContext, ids: ID[]) {
     updateVariantsById(ctx: RequestContext, ids: ID[]) {
-        this.addJobToQueue({ type: 'update-variants-by-id', ctx: ctx.serialize(), ids });
+        this.updateIndexQueue.add({ type: 'update-variants-by-id', ctx: ctx.serialize(), ids });
     }
     }
 
 
     updateAsset(ctx: RequestContext, asset: Asset) {
     updateAsset(ctx: RequestContext, asset: Asset) {
-        this.addJobToQueue({ type: 'update-asset', ctx: ctx.serialize(), asset: asset as any });
+        this.updateIndexQueue.add({ type: 'update-asset', ctx: ctx.serialize(), asset: asset as any });
     }
     }
 
 
     deleteAsset(ctx: RequestContext, asset: Asset) {
     deleteAsset(ctx: RequestContext, asset: Asset) {
-        this.addJobToQueue({ type: 'delete-asset', ctx: ctx.serialize(), asset: asset as any });
+        this.updateIndexQueue.add({ type: 'delete-asset', ctx: ctx.serialize(), asset: asset as any });
     }
     }
 
 
     assignProductToChannel(ctx: RequestContext, productId: ID, channelId: ID) {
     assignProductToChannel(ctx: RequestContext, productId: ID, channelId: ID) {
-        this.addJobToQueue({
+        this.updateIndexQueue.add({
             type: 'assign-product-to-channel',
             type: 'assign-product-to-channel',
             ctx: ctx.serialize(),
             ctx: ctx.serialize(),
             productId,
             productId,
@@ -121,7 +107,7 @@ export class SearchIndexService {
     }
     }
 
 
     removeProductFromChannel(ctx: RequestContext, productId: ID, channelId: ID) {
     removeProductFromChannel(ctx: RequestContext, productId: ID, channelId: ID) {
-        this.addJobToQueue({
+        this.updateIndexQueue.add({
             type: 'remove-product-from-channel',
             type: 'remove-product-from-channel',
             ctx: ctx.serialize(),
             ctx: ctx.serialize(),
             productId,
             productId,
@@ -130,7 +116,7 @@ export class SearchIndexService {
     }
     }
 
 
     assignVariantToChannel(ctx: RequestContext, productVariantId: ID, channelId: ID) {
     assignVariantToChannel(ctx: RequestContext, productVariantId: ID, channelId: ID) {
-        this.addJobToQueue({
+        this.updateIndexQueue.add({
             type: 'assign-variant-to-channel',
             type: 'assign-variant-to-channel',
             ctx: ctx.serialize(),
             ctx: ctx.serialize(),
             productVariantId,
             productVariantId,
@@ -139,7 +125,7 @@ export class SearchIndexService {
     }
     }
 
 
     removeVariantFromChannel(ctx: RequestContext, productVariantId: ID, channelId: ID) {
     removeVariantFromChannel(ctx: RequestContext, productVariantId: ID, channelId: ID) {
-        this.addJobToQueue({
+        this.updateIndexQueue.add({
             type: 'remove-variant-from-channel',
             type: 'remove-variant-from-channel',
             ctx: ctx.serialize(),
             ctx: ctx.serialize(),
             productVariantId,
             productVariantId,
@@ -147,30 +133,15 @@ export class SearchIndexService {
         });
         });
     }
     }
 
 
-    private addJobToQueue(data: UpdateIndexQueueJobData) {
-        if (updateIndexQueue) {
-            return updateIndexQueue.add(data);
-        }
-    }
-
-    private sendMessage(job: Job<any>, message: WorkerMessage<any, any>): Promise<any> {
-        return new Promise((resolve, reject) => {
-            this.workerService.send(message).subscribe({
-                complete: () => resolve(),
-                error: err => {
-                    Logger.error(err);
-                    reject(err);
-                },
-            });
-        });
-    }
-
-    private sendMessageWithProgress(job: Job<any>, message: ReindexMessage | UpdateVariantsByIdMessage): Promise<any> {
+    private jobWithProgress(
+        job: Job<UpdateIndexQueueJobData>,
+        ob: Observable<ReindexMessageResponse>,
+    ): Promise<any> {
         return new Promise((resolve, reject) => {
         return new Promise((resolve, reject) => {
             let total: number | undefined;
             let total: number | undefined;
             let duration = 0;
             let duration = 0;
             let completed = 0;
             let completed = 0;
-            this.workerService.send(message).subscribe({
+            ob.subscribe({
                 next: (response: ReindexMessageResponse) => {
                 next: (response: ReindexMessageResponse) => {
                     if (!total) {
                     if (!total) {
                         total = response.total;
                         total = response.total;

+ 0 - 41
packages/core/src/plugin/default-search-plugin/types.ts

@@ -2,7 +2,6 @@ import { ID, JsonCompatible } from '@vendure/common/lib/shared-types';
 
 
 import { SerializedRequestContext } from '../../api/common/request-context';
 import { SerializedRequestContext } from '../../api/common/request-context';
 import { Asset } from '../../entity/asset/asset.entity';
 import { Asset } from '../../entity/asset/asset.entity';
-import { WorkerMessage } from '../../worker/types';
 
 
 export type ReindexMessageResponse = {
 export type ReindexMessageResponse = {
     total: number;
     total: number;
@@ -46,46 +45,6 @@ export type VariantChannelMessageData = {
     channelId: ID;
     channelId: ID;
 };
 };
 
 
-export class ReindexMessage extends WorkerMessage<ReindexMessageData, ReindexMessageResponse> {
-    static readonly pattern = 'Reindex';
-}
-export class UpdateVariantMessage extends WorkerMessage<UpdateVariantMessageData, boolean> {
-    static readonly pattern = 'UpdateProduct';
-}
-export class UpdateProductMessage extends WorkerMessage<UpdateProductMessageData, boolean> {
-    static readonly pattern = 'UpdateVariant';
-}
-export class DeleteVariantMessage extends WorkerMessage<UpdateVariantMessageData, boolean> {
-    static readonly pattern = 'DeleteProduct';
-}
-export class DeleteProductMessage extends WorkerMessage<UpdateProductMessageData, boolean> {
-    static readonly pattern = 'DeleteVariant';
-}
-export class UpdateVariantsByIdMessage extends WorkerMessage<
-    UpdateVariantsByIdMessageData,
-    ReindexMessageResponse
-> {
-    static readonly pattern = 'UpdateVariantsById';
-}
-export class AssignProductToChannelMessage extends WorkerMessage<ProductChannelMessageData, boolean> {
-    static readonly pattern = 'AssignProductToChannel';
-}
-export class RemoveProductFromChannelMessage extends WorkerMessage<ProductChannelMessageData, boolean> {
-    static readonly pattern = 'RemoveProductFromChannel';
-}
-export class AssignVariantToChannelMessage extends WorkerMessage<VariantChannelMessageData, boolean> {
-    static readonly pattern = 'AssignVariantToChannel';
-}
-export class RemoveVariantFromChannelMessage extends WorkerMessage<VariantChannelMessageData, boolean> {
-    static readonly pattern = 'RemoveVariantFromChannel';
-}
-export class UpdateAssetMessage extends WorkerMessage<UpdateAssetMessageData, boolean> {
-    static readonly pattern = 'UpdateAsset';
-}
-export class DeleteAssetMessage extends WorkerMessage<UpdateAssetMessageData, boolean> {
-    static readonly pattern = 'DeleteAsset';
-}
-
 type NamedJobData<Type extends string, MessageData> = { type: Type } & MessageData;
 type NamedJobData<Type extends string, MessageData> = { type: Type } & MessageData;
 
 
 export type ReindexJobData = NamedJobData<'reindex', ReindexMessageData>;
 export type ReindexJobData = NamedJobData<'reindex', ReindexMessageData>;

+ 3 - 4
packages/core/src/plugin/plugin-common.module.ts

@@ -1,11 +1,11 @@
 import { Module } from '@nestjs/common';
 import { Module } from '@nestjs/common';
 
 
+import { CacheModule } from '../cache/cache.module';
 import { ConfigModule } from '../config/config.module';
 import { ConfigModule } from '../config/config.module';
 import { EventBusModule } from '../event-bus/event-bus.module';
 import { EventBusModule } from '../event-bus/event-bus.module';
 import { HealthCheckModule } from '../health-check/health-check.module';
 import { HealthCheckModule } from '../health-check/health-check.module';
 import { JobQueueModule } from '../job-queue/job-queue.module';
 import { JobQueueModule } from '../job-queue/job-queue.module';
 import { ServiceModule } from '../service/service.module';
 import { ServiceModule } from '../service/service.module';
-import { WorkerServiceModule } from '../worker/worker-service.module';
 
 
 /**
 /**
  * @description
  * @description
@@ -18,7 +18,6 @@ import { WorkerServiceModule } from '../worker/worker-service.module';
  * * `EventBusModule`, allowing the injection of the {@link EventBus} service.
  * * `EventBusModule`, allowing the injection of the {@link EventBus} service.
  * * `ServiceModule` allowing the injection of any of the various entity services such as ProductService, OrderService etc.
  * * `ServiceModule` allowing the injection of any of the various entity services such as ProductService, OrderService etc.
  * * `ConfigModule`, allowing the injection of the ConfigService.
  * * `ConfigModule`, allowing the injection of the ConfigService.
- * * `WorkerServiceModule`, allowing the injection of the {@link WorkerService}.
  * * `JobQueueModule`, allowing the injection of the {@link JobQueueService}.
  * * `JobQueueModule`, allowing the injection of the {@link JobQueueService}.
  * * `HealthCheckModule`, allowing the injection of the {@link HealthCheckRegistryService}.
  * * `HealthCheckModule`, allowing the injection of the {@link HealthCheckRegistryService}.
  *
  *
@@ -29,17 +28,17 @@ import { WorkerServiceModule } from '../worker/worker-service.module';
         EventBusModule,
         EventBusModule,
         ConfigModule,
         ConfigModule,
         ServiceModule.forPlugin(),
         ServiceModule.forPlugin(),
-        WorkerServiceModule,
         JobQueueModule,
         JobQueueModule,
         HealthCheckModule,
         HealthCheckModule,
+        CacheModule,
     ],
     ],
     exports: [
     exports: [
         EventBusModule,
         EventBusModule,
         ConfigModule,
         ConfigModule,
         ServiceModule.forPlugin(),
         ServiceModule.forPlugin(),
-        WorkerServiceModule,
         JobQueueModule,
         JobQueueModule,
         HealthCheckModule,
         HealthCheckModule,
+        CacheModule,
     ],
     ],
 })
 })
 export class PluginCommonModule {}
 export class PluginCommonModule {}

+ 1 - 13
packages/core/src/plugin/plugin-metadata.ts

@@ -4,13 +4,12 @@ import { Type } from '@vendure/common/lib/shared-types';
 
 
 import { notNullOrUndefined } from '../../../common/lib/shared-utils';
 import { notNullOrUndefined } from '../../../common/lib/shared-utils';
 
 
-import { APIExtensionDefinition, PluginConfigurationFn, PluginLifecycleMethods } from './vendure-plugin';
+import { APIExtensionDefinition, PluginConfigurationFn } from './vendure-plugin';
 
 
 export const PLUGIN_METADATA = {
 export const PLUGIN_METADATA = {
     CONFIGURATION: 'configuration',
     CONFIGURATION: 'configuration',
     SHOP_API_EXTENSIONS: 'shopApiExtensions',
     SHOP_API_EXTENSIONS: 'shopApiExtensions',
     ADMIN_API_EXTENSIONS: 'adminApiExtensions',
     ADMIN_API_EXTENSIONS: 'adminApiExtensions',
-    WORKERS: 'workers',
     ENTITIES: 'entities',
     ENTITIES: 'entities',
 };
 };
 
 
@@ -48,17 +47,6 @@ export function getPluginModules(plugins: Array<Type<any> | DynamicModule>): Arr
     return plugins.map(p => (isDynamicModule(p) ? p.module : p));
     return plugins.map(p => (isDynamicModule(p) ? p.module : p));
 }
 }
 
 
-export function hasLifecycleMethod<M extends keyof PluginLifecycleMethods>(
-    plugin: any,
-    lifecycleMethod: M,
-): plugin is { [key in M]: PluginLifecycleMethods[M] } {
-    return typeof (plugin as any)[lifecycleMethod] === 'function';
-}
-
-export function getWorkerControllers(plugin: Type<any> | DynamicModule) {
-    return reflectMetadata(plugin, PLUGIN_METADATA.WORKERS);
-}
-
 export function getConfigurationFunction(
 export function getConfigurationFunction(
     plugin: Type<any> | DynamicModule,
     plugin: Type<any> | DynamicModule,
 ): PluginConfigurationFn | undefined {
 ): PluginConfigurationFn | undefined {

+ 2 - 93
packages/core/src/plugin/plugin.module.ts

@@ -1,25 +1,7 @@
-import { DynamicModule, Inject, Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
-import { ModuleRef } from '@nestjs/core';
+import { DynamicModule, Module } from '@nestjs/common';
 
 
 import { getConfig } from '../config/config-helpers';
 import { getConfig } from '../config/config-helpers';
 import { ConfigModule } from '../config/config.module';
 import { ConfigModule } from '../config/config.module';
-import { ConfigService } from '../config/config.service';
-import { Logger } from '../config/logger/vendure-logger';
-
-import {
-    getPluginModules,
-    getWorkerControllers,
-    hasLifecycleMethod,
-    isDynamicModule,
-} from './plugin-metadata';
-import { PluginLifecycleMethods } from './vendure-plugin';
-
-export enum PluginProcessContext {
-    Main,
-    Worker,
-}
-
-const PLUGIN_PROCESS_CONTEXT = 'PLUGIN_PROCESS_CONTEXT';
 
 
 /**
 /**
  * This module collects and re-exports all providers defined in plugins so that they can be used in other
  * This module collects and re-exports all providers defined in plugins so that they can be used in other
@@ -28,84 +10,11 @@ const PLUGIN_PROCESS_CONTEXT = 'PLUGIN_PROCESS_CONTEXT';
 @Module({
 @Module({
     imports: [ConfigModule],
     imports: [ConfigModule],
 })
 })
-export class PluginModule implements OnModuleInit, OnModuleDestroy {
+export class PluginModule {
     static forRoot(): DynamicModule {
     static forRoot(): DynamicModule {
         return {
         return {
             module: PluginModule,
             module: PluginModule,
-            providers: [{ provide: PLUGIN_PROCESS_CONTEXT, useValue: PluginProcessContext.Main }],
             imports: [...getConfig().plugins],
             imports: [...getConfig().plugins],
         };
         };
     }
     }
-    static forWorker(): DynamicModule {
-        return {
-            module: PluginModule,
-            providers: [{ provide: PLUGIN_PROCESS_CONTEXT, useValue: PluginProcessContext.Worker }],
-            imports: [...pluginsWithWorkerControllers()],
-        };
-    }
-    constructor(
-        @Inject(PLUGIN_PROCESS_CONTEXT) private processContext: PluginProcessContext,
-        private moduleRef: ModuleRef,
-        private configService: ConfigService,
-    ) {}
-
-    async onModuleInit() {
-        if (this.processContext === PluginProcessContext.Main) {
-            await this.runPluginLifecycleMethods('onVendureBootstrap', instance => {
-                const pluginName = instance.constructor.name || '(anonymous plugin)';
-                Logger.verbose(`Bootstrapped plugin ${pluginName}`);
-            });
-        }
-        if (this.processContext === PluginProcessContext.Worker) {
-            await this.runPluginLifecycleMethods('onVendureWorkerBootstrap');
-        }
-    }
-
-    async onModuleDestroy() {
-        if (this.processContext === PluginProcessContext.Main) {
-            await this.runPluginLifecycleMethods('onVendureClose');
-        }
-        if (this.processContext === PluginProcessContext.Worker) {
-            await this.runPluginLifecycleMethods('onVendureWorkerClose');
-        }
-    }
-
-    private async runPluginLifecycleMethods(
-        lifecycleMethod: keyof PluginLifecycleMethods,
-        afterRun?: (instance: any) => void,
-    ) {
-        for (const plugin of getPluginModules(this.configService.plugins)) {
-            let instance: any;
-            try {
-                instance = this.moduleRef.get(plugin, { strict: false });
-            } catch (e) {
-                Logger.error(`Could not find ${plugin.name}`, undefined, e.stack);
-            }
-            if (instance) {
-                if (hasLifecycleMethod(instance, lifecycleMethod)) {
-                    await instance[lifecycleMethod]();
-                }
-                if (typeof afterRun === 'function') {
-                    afterRun(instance);
-                }
-            }
-        }
-    }
-}
-
-function pluginsWithWorkerControllers(): DynamicModule[] {
-    return getConfig().plugins.map(plugin => {
-        const controllers = getWorkerControllers(plugin);
-        if (isDynamicModule(plugin)) {
-            return {
-                ...plugin,
-                controllers,
-            };
-        } else {
-            return {
-                module: plugin,
-                controllers,
-            };
-        }
-    });
 }
 }

+ 0 - 85
packages/core/src/plugin/vendure-plugin.ts

@@ -37,12 +37,6 @@ export interface VendurePluginMetadata extends ModuleMetadata {
      * schema definitions and any required resolvers.
      * schema definitions and any required resolvers.
      */
      */
     adminApiExtensions?: APIExtensionDefinition;
     adminApiExtensions?: APIExtensionDefinition;
-    /**
-     * @description
-     * The plugin may define [Nestjs microservice controllers](https://docs.nestjs.com/microservices/basics#request-response)
-     * which are run in the Worker context.
-     */
-    workers?: Array<Type<any>>;
     /**
     /**
      * @description
      * @description
      * The plugin may define custom [TypeORM database entities](https://typeorm.io/#/entities).
      * The plugin may define custom [TypeORM database entities](https://typeorm.io/#/entities).
@@ -138,82 +132,3 @@ export function VendurePlugin(pluginMetadata: VendurePluginMetadata): ClassDecor
         Module(nestModuleMetadata)(target);
         Module(nestModuleMetadata)(target);
     };
     };
 }
 }
-
-/**
- * @description
- * A plugin which implements a static `beforeVendureBootstrap` method with this type can define logic to run
- * before the Vendure server (and the underlying Nestjs application) is bootstrapped. This is called
- * _after_ the Nestjs application has been created, but _before_ the `app.listen()` method is invoked.
- *
- * @docsCategory plugin
- * @docsPage Plugin Lifecycle Methods
- */
-export type BeforeVendureBootstrap = (app: INestApplication) => void | Promise<void>;
-
-/**
- * @description
- * A plugin which implements a static `beforeVendureWorkerBootstrap` method with this type can define logic to run
- * before the Vendure worker (and the underlying Nestjs microservice) is bootstrapped. This is called
- * _after_ the Nestjs microservice has been created, but _before_ the `microservice.listen()` method is invoked.
- *
- * @docsCategory plugin
- * @docsPage Plugin Lifecycle Methods
- */
-export type BeforeVendureWorkerBootstrap = (app: INestMicroservice) => void | Promise<void>;
-
-/**
- * @description
- * A plugin which implements this interface can define logic to run when the Vendure server is initialized.
- *
- * For example, this could be used to call out to an external API or to set up {@link EventBus} listeners.
- *
- * @docsCategory plugin
- * @docsPage Plugin Lifecycle Methods
- */
-export interface OnVendureBootstrap {
-    onVendureBootstrap(): void | Promise<void>;
-}
-
-/**
- * @description
- * A plugin which implements this interface can define logic to run when the Vendure worker is initialized.
- *
- * For example, this could be used to start or connect to a server or databased used by the worker.
- *
- * @docsCategory plugin
- * @docsPage Plugin Lifecycle Methods
- */
-export interface OnVendureWorkerBootstrap {
-    onVendureWorkerBootstrap(): void | Promise<void>;
-}
-
-/**
- * @description
- * A plugin which implements this interface can define logic to run before Vendure server is closed.
- *
- * For example, this could be used to clean up any processes started by the {@link OnVendureBootstrap} method.
- *
- * @docsCategory plugin
- * @docsPage Plugin Lifecycle Methods
- */
-export interface OnVendureClose {
-    onVendureClose(): void | Promise<void>;
-}
-
-/**
- * @description
- * A plugin which implements this interface can define logic to run before Vendure worker is closed.
- *
- * For example, this could be used to close any open connections to external services.
- *
- * @docsCategory plugin
- * @docsPage Plugin Lifecycle Methods
- */
-export interface OnVendureWorkerClose {
-    onVendureWorkerClose(): void | Promise<void>;
-}
-
-export type PluginLifecycleMethods = OnVendureBootstrap &
-    OnVendureWorkerBootstrap &
-    OnVendureClose &
-    OnVendureWorkerClose;

+ 0 - 2
packages/core/src/process-context/index.ts

@@ -1,2 +0,0 @@
-export * from './process-context';
-export * from './process-context.module';

+ 0 - 22
packages/core/src/process-context/process-context.module.ts

@@ -1,22 +0,0 @@
-import { DynamicModule, Global, Module } from '@nestjs/common';
-
-import { ProcessContext, ServerProcessContext, WorkerProcessContext } from './process-context';
-
-@Global()
-@Module({})
-export class ProcessContextModule {
-    static forRoot(): DynamicModule {
-        return {
-            module: ProcessContextModule,
-            providers: [{ provide: ProcessContext, useClass: ServerProcessContext }],
-            exports: [ProcessContext],
-        };
-    }
-    static forWorker(): DynamicModule {
-        return {
-            module: ProcessContextModule,
-            providers: [{ provide: ProcessContext, useClass: WorkerProcessContext }],
-            exports: [ProcessContext],
-        };
-    }
-}

+ 0 - 30
packages/core/src/process-context/process-context.ts

@@ -1,30 +0,0 @@
-import { Injectable } from '@nestjs/common';
-
-/**
- * @description
- * The ProcessContext can be injected into your providers in order to know whether that provider
- * is being executed in the context of the main Vendure server or the worker.
- *
- * @docsCategory common
- */
-@Injectable()
-export class ProcessContext {
-    protected _isServer: boolean;
-
-    get isServer(): boolean {
-        return this._isServer;
-    }
-    get isWorker(): boolean {
-        return !this._isServer;
-    }
-}
-
-@Injectable()
-export class ServerProcessContext extends ProcessContext {
-    protected _isServer = true;
-}
-
-@Injectable()
-export class WorkerProcessContext extends ProcessContext {
-    protected _isServer = false;
-}

+ 0 - 117
packages/core/src/service/controllers/collection.controller.ts

@@ -1,117 +0,0 @@
-import { Controller } from '@nestjs/common';
-import { MessagePattern } from '@nestjs/microservices';
-import { ConfigurableOperation } from '@vendure/common/lib/generated-types';
-import { pick } from '@vendure/common/lib/pick';
-import { ID } from '@vendure/common/lib/shared-types';
-import { Observable } from 'rxjs';
-
-import { ConfigService } from '../../config/config.service';
-import { Logger } from '../../config/logger/vendure-logger';
-import { Collection } from '../../entity/collection/collection.entity';
-import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
-import { asyncObservable } from '../../worker/async-observable';
-import { CollectionService } from '../services/collection.service';
-import { TransactionalConnection } from '../transaction/transactional-connection';
-import { ApplyCollectionFiltersMessage } from '../types/collection-messages';
-
-/**
- * Updates collections on the worker process because running the CollectionFilters
- * is computationally expensive.
- */
-@Controller()
-export class CollectionController {
-    constructor(
-        private connection: TransactionalConnection,
-        private collectionService: CollectionService,
-        private configService: ConfigService,
-    ) {}
-
-    @MessagePattern(ApplyCollectionFiltersMessage.pattern)
-    applyCollectionFilters({
-        collectionIds,
-    }: ApplyCollectionFiltersMessage['data']): Observable<ApplyCollectionFiltersMessage['response']> {
-        return asyncObservable(async observer => {
-            Logger.verbose(`Processing ${collectionIds.length} Collections`);
-            const timeStart = Date.now();
-            const collections = await this.connection.getRepository(Collection).findByIds(collectionIds, {
-                relations: ['productVariants'],
-            });
-            let completed = 0;
-            for (const collection of collections) {
-                const affectedVariantIds = await this.applyCollectionFiltersInternal(collection);
-
-                observer.next({
-                    total: collectionIds.length,
-                    completed: ++completed,
-                    duration: +new Date() - timeStart,
-                    collectionId: collection.id,
-                    affectedVariantIds,
-                });
-            }
-        });
-    }
-
-    /**
-     * Applies the CollectionFilters and returns an array of all affected ProductVariant ids.
-     */
-    private async applyCollectionFiltersInternal(collection: Collection): Promise<ID[]> {
-        const ancestorFilters = await this.collectionService
-            .getAncestors(collection.id)
-            .then(ancestors =>
-                ancestors.reduce(
-                    (filters, c) => [...filters, ...(c.filters || [])],
-                    [] as ConfigurableOperation[],
-                ),
-            );
-        const preIds = await this.collectionService.getCollectionProductVariantIds(collection);
-        collection.productVariants = await this.getFilteredProductVariants([
-            ...ancestorFilters,
-            ...(collection.filters || []),
-        ]);
-        const postIds = collection.productVariants.map(v => v.id);
-        try {
-            await this.connection
-                .getRepository(Collection)
-                // Only update the exact changed properties, to avoid VERY hard-to-debug
-                // non-deterministic race conditions e.g. when the "position" is changed
-                // by moving a Collection and then this save operation clobbers it back
-                // to the old value.
-                .save(pick(collection, ['id', 'productVariants']), {
-                    chunk: Math.ceil(collection.productVariants.length / 500),
-                    reload: false,
-                });
-        } catch (e) {
-            Logger.error(e);
-        }
-
-        const preIdsSet = new Set(preIds);
-        const postIdsSet = new Set(postIds);
-        const difference = [
-            ...preIds.filter(id => !postIdsSet.has(id)),
-            ...postIds.filter(id => !preIdsSet.has(id)),
-        ];
-        return difference;
-    }
-
-    /**
-     * Applies the CollectionFilters and returns an array of ProductVariant entities which match.
-     */
-    private async getFilteredProductVariants(filters: ConfigurableOperation[]): Promise<ProductVariant[]> {
-        if (filters.length === 0) {
-            return [];
-        }
-        const { collectionFilters } = this.configService.catalogOptions;
-        let qb = this.connection.getRepository(ProductVariant).createQueryBuilder('productVariant');
-
-        for (const filterType of collectionFilters) {
-            const filtersOfType = filters.filter(f => f.code === filterType.code);
-            if (filtersOfType.length) {
-                for (const filter of filtersOfType) {
-                    qb = filterType.apply(qb, filter.args);
-                }
-            }
-        }
-
-        return qb.getMany();
-    }
-}

+ 0 - 22
packages/core/src/service/controllers/tax-rate.controller.ts

@@ -1,22 +0,0 @@
-import { Controller } from '@nestjs/common';
-import { MessagePattern } from '@nestjs/microservices';
-import { from, Observable } from 'rxjs';
-
-import { RequestContext } from '../../api/common/request-context';
-import { TaxRateService } from '../services/tax-rate.service';
-import { TransactionalConnection } from '../transaction/transactional-connection';
-import { TaxRateUpdatedMessage } from '../types/tax-rate-messages';
-
-@Controller()
-export class TaxRateController {
-    constructor(private connection: TransactionalConnection, private taxRateService: TaxRateService) {}
-
-    /**
-     * When a TaxRate is updated on the main process, this will update the activeTaxRates
-     * cache on the worker.
-     */
-    @MessagePattern(TaxRateUpdatedMessage.pattern)
-    taxRateUpdated(): Observable<TaxRateUpdatedMessage['response']> {
-        return from(this.taxRateService.updateActiveTaxRates(RequestContext.empty()).then(() => true));
-    }
-}

+ 0 - 2
packages/core/src/service/helpers/order-calculator/order-calculator.spec.ts

@@ -27,7 +27,6 @@ import {
     taxCategoryStandard,
     taxCategoryStandard,
     taxCategoryZero,
     taxCategoryZero,
 } from '../../../testing/order-test-utils';
 } from '../../../testing/order-test-utils';
-import { WorkerService } from '../../../worker/worker.service';
 import { ShippingMethodService } from '../../services/shipping-method.service';
 import { ShippingMethodService } from '../../services/shipping-method.service';
 import { TaxRateService } from '../../services/tax-rate.service';
 import { TaxRateService } from '../../services/tax-rate.service';
 import { ZoneService } from '../../services/zone.service';
 import { ZoneService } from '../../services/zone.service';
@@ -1559,7 +1558,6 @@ function createTestModule() {
             { provide: ListQueryBuilder, useValue: {} },
             { provide: ListQueryBuilder, useValue: {} },
             { provide: ConfigService, useClass: MockConfigService },
             { provide: ConfigService, useClass: MockConfigService },
             { provide: EventBus, useValue: { publish: () => ({}) } },
             { provide: EventBus, useValue: { publish: () => ({}) } },
-            { provide: WorkerService, useValue: { send: () => ({}) } },
             { provide: ZoneService, useValue: { findAll: () => [] } },
             { provide: ZoneService, useValue: { findAll: () => [] } },
         ],
         ],
     }).compile();
     }).compile();

+ 10 - 7
packages/core/src/service/helpers/order-calculator/order-calculator.ts

@@ -59,7 +59,7 @@ export class OrderCalculator {
                 order,
                 order,
                 updatedOrderLine,
                 updatedOrderLine,
                 activeTaxZone,
                 activeTaxZone,
-                this.createTaxRateGetter(activeTaxZone),
+                this.createTaxRateGetter(ctx, activeTaxZone),
             );
             );
             updatedOrderLine.activeItems.forEach(item => updatedOrderItems.add(item));
             updatedOrderLine.activeItems.forEach(item => updatedOrderItems.add(item));
         }
         }
@@ -93,7 +93,7 @@ export class OrderCalculator {
      * Applies the correct TaxRate to each OrderItem in the order.
      * Applies the correct TaxRate to each OrderItem in the order.
      */
      */
     private async applyTaxes(ctx: RequestContext, order: Order, activeZone: Zone) {
     private async applyTaxes(ctx: RequestContext, order: Order, activeZone: Zone) {
-        const getTaxRate = this.createTaxRateGetter(activeZone);
+        const getTaxRate = this.createTaxRateGetter(ctx, activeZone);
         for (const line of order.lines) {
         for (const line of order.lines) {
             await this.applyTaxesToOrderLine(ctx, order, line, activeZone, getTaxRate);
             await this.applyTaxesToOrderLine(ctx, order, line, activeZone, getTaxRate);
         }
         }
@@ -108,9 +108,9 @@ export class OrderCalculator {
         order: Order,
         order: Order,
         line: OrderLine,
         line: OrderLine,
         activeZone: Zone,
         activeZone: Zone,
-        getTaxRate: (taxCategory: TaxCategory) => TaxRate,
+        getTaxRate: (taxCategory: TaxCategory) => Promise<TaxRate>,
     ) {
     ) {
-        const applicableTaxRate = getTaxRate(line.taxCategory);
+        const applicableTaxRate = await getTaxRate(line.taxCategory);
         const { taxLineCalculationStrategy } = this.configService.taxOptions;
         const { taxLineCalculationStrategy } = this.configService.taxOptions;
         for (const item of line.activeItems) {
         for (const item of line.activeItems) {
             item.taxLines = await taxLineCalculationStrategy.calculate({
             item.taxLines = await taxLineCalculationStrategy.calculate({
@@ -127,15 +127,18 @@ export class OrderCalculator {
      * Returns a memoized function for performing an efficient
      * Returns a memoized function for performing an efficient
      * lookup of the correct TaxRate for a given TaxCategory.
      * lookup of the correct TaxRate for a given TaxCategory.
      */
      */
-    private createTaxRateGetter(activeZone: Zone): (taxCategory: TaxCategory) => TaxRate {
+    private createTaxRateGetter(
+        ctx: RequestContext,
+        activeZone: Zone,
+    ): (taxCategory: TaxCategory) => Promise<TaxRate> {
         const taxRateCache = new Map<TaxCategory, TaxRate>();
         const taxRateCache = new Map<TaxCategory, TaxRate>();
 
 
-        return (taxCategory: TaxCategory): TaxRate => {
+        return async (taxCategory: TaxCategory): Promise<TaxRate> => {
             const cached = taxRateCache.get(taxCategory);
             const cached = taxRateCache.get(taxCategory);
             if (cached) {
             if (cached) {
                 return cached;
                 return cached;
             }
             }
-            const rate = this.taxRateService.getApplicableTaxRate(activeZone, taxCategory);
+            const rate = await this.taxRateService.getApplicableTaxRate(ctx, activeZone, taxCategory);
             taxRateCache.set(taxCategory, rate);
             taxRateCache.set(taxCategory, rate);
             return rate;
             return rate;
         };
         };

+ 1 - 1
packages/core/src/service/helpers/order-modifier/order-modifier.ts

@@ -125,7 +125,7 @@ export class OrderModifier {
             ],
             ],
         });
         });
         lineWithRelations.productVariant = translateDeep(
         lineWithRelations.productVariant = translateDeep(
-            this.productVariantService.applyChannelPriceAndTax(lineWithRelations.productVariant, ctx),
+            await this.productVariantService.applyChannelPriceAndTax(lineWithRelations.productVariant, ctx),
             ctx.languageCode,
             ctx.languageCode,
         );
         );
         order.lines.push(lineWithRelations);
         order.lines.push(lineWithRelations);

+ 0 - 3
packages/core/src/service/initializer.service.ts

@@ -5,7 +5,6 @@ import { ChannelService } from './services/channel.service';
 import { GlobalSettingsService } from './services/global-settings.service';
 import { GlobalSettingsService } from './services/global-settings.service';
 import { RoleService } from './services/role.service';
 import { RoleService } from './services/role.service';
 import { ShippingMethodService } from './services/shipping-method.service';
 import { ShippingMethodService } from './services/shipping-method.service';
-import { TaxRateService } from './services/tax-rate.service';
 
 
 /**
 /**
  * Only used internally to run the various service init methods in the correct
  * Only used internally to run the various service init methods in the correct
@@ -17,7 +16,6 @@ export class InitializerService {
         private channelService: ChannelService,
         private channelService: ChannelService,
         private roleService: RoleService,
         private roleService: RoleService,
         private administratorService: AdministratorService,
         private administratorService: AdministratorService,
-        private taxRateService: TaxRateService,
         private shippingMethodService: ShippingMethodService,
         private shippingMethodService: ShippingMethodService,
         private globalSettingsService: GlobalSettingsService,
         private globalSettingsService: GlobalSettingsService,
     ) {}
     ) {}
@@ -33,7 +31,6 @@ export class InitializerService {
         await this.channelService.initChannels();
         await this.channelService.initChannels();
         await this.roleService.initRoles();
         await this.roleService.initRoles();
         await this.administratorService.initAdministrators();
         await this.administratorService.initAdministrators();
-        await this.taxRateService.initTaxRates();
         await this.shippingMethodService.initShippingMethods();
         await this.shippingMethodService.initShippingMethods();
     }
     }
 }
 }

+ 2 - 39
packages/core/src/service/service.module.ts

@@ -2,15 +2,13 @@ import { DynamicModule, Module } from '@nestjs/common';
 import { TypeOrmModule } from '@nestjs/typeorm';
 import { TypeOrmModule } from '@nestjs/typeorm';
 import { ConnectionOptions } from 'typeorm';
 import { ConnectionOptions } from 'typeorm';
 
 
+import { CacheModule } from '../cache/cache.module';
 import { ConfigModule } from '../config/config.module';
 import { ConfigModule } from '../config/config.module';
 import { ConfigService } from '../config/config.service';
 import { ConfigService } from '../config/config.service';
 import { TypeOrmLogger } from '../config/logger/typeorm-logger';
 import { TypeOrmLogger } from '../config/logger/typeorm-logger';
 import { EventBusModule } from '../event-bus/event-bus.module';
 import { EventBusModule } from '../event-bus/event-bus.module';
 import { JobQueueModule } from '../job-queue/job-queue.module';
 import { JobQueueModule } from '../job-queue/job-queue.module';
-import { WorkerServiceModule } from '../worker/worker-service.module';
 
 
-import { CollectionController } from './controllers/collection.controller';
-import { TaxRateController } from './controllers/tax-rate.controller';
 import { ConfigArgService } from './helpers/config-arg/config-arg.service';
 import { ConfigArgService } from './helpers/config-arg/config-arg.service';
 import { CustomFieldRelationService } from './helpers/custom-field-relation/custom-field-relation.service';
 import { CustomFieldRelationService } from './helpers/custom-field-relation/custom-field-relation.service';
 import { ExternalAuthenticationService } from './helpers/external-authentication/external-authentication.service';
 import { ExternalAuthenticationService } from './helpers/external-authentication/external-authentication.service';
@@ -117,8 +115,6 @@ const helpers = [
     CustomFieldRelationService,
     CustomFieldRelationService,
 ];
 ];
 
 
-const workerControllers = [CollectionController, TaxRateController];
-
 let defaultTypeOrmModule: DynamicModule;
 let defaultTypeOrmModule: DynamicModule;
 let workerTypeOrmModule: DynamicModule;
 let workerTypeOrmModule: DynamicModule;
 
 
@@ -128,7 +124,7 @@ let workerTypeOrmModule: DynamicModule;
  * only run a single time.
  * only run a single time.
  */
  */
 @Module({
 @Module({
-    imports: [ConfigModule, EventBusModule, WorkerServiceModule, JobQueueModule],
+    imports: [ConfigModule, EventBusModule, CacheModule, JobQueueModule],
     providers: [...services, ...helpers, InitializerService],
     providers: [...services, ...helpers, InitializerService],
     exports: [...services, ...helpers],
     exports: [...services, ...helpers],
 })
 })
@@ -167,39 +163,6 @@ export class ServiceModule {
         };
         };
     }
     }
 
 
-    static forWorker(): DynamicModule {
-        if (!workerTypeOrmModule) {
-            workerTypeOrmModule = TypeOrmModule.forRootAsync({
-                imports: [ConfigModule],
-                useFactory: (configService: ConfigService) => {
-                    const { dbConnectionOptions, workerOptions } = configService;
-                    const logger = ServiceModule.getTypeOrmLogger(dbConnectionOptions);
-                    if (workerOptions.runInMainProcess) {
-                        // When running in the main process, we can re-use the existing
-                        // default connection.
-                        return {
-                            ...dbConnectionOptions,
-                            logger,
-                            name: 'default',
-                            keepConnectionAlive: true,
-                        };
-                    } else {
-                        return {
-                            ...dbConnectionOptions,
-                            logger,
-                        };
-                    }
-                },
-                inject: [ConfigService],
-            });
-        }
-        return {
-            module: ServiceModule,
-            imports: [workerTypeOrmModule, ConfigModule],
-            controllers: workerControllers,
-        };
-    }
-
     static forPlugin(): DynamicModule {
     static forPlugin(): DynamicModule {
         return {
         return {
             module: ServiceModule,
             module: ServiceModule,

+ 75 - 38
packages/core/src/service/services/collection.service.ts

@@ -28,10 +28,8 @@ import { EventBus } from '../../event-bus/event-bus';
 import { CollectionModificationEvent } from '../../event-bus/events/collection-modification-event';
 import { CollectionModificationEvent } from '../../event-bus/events/collection-modification-event';
 import { ProductEvent } from '../../event-bus/events/product-event';
 import { ProductEvent } from '../../event-bus/events/product-event';
 import { ProductVariantEvent } from '../../event-bus/events/product-variant-event';
 import { ProductVariantEvent } from '../../event-bus/events/product-variant-event';
-import { Job } from '../../job-queue/job';
 import { JobQueue } from '../../job-queue/job-queue';
 import { JobQueue } from '../../job-queue/job-queue';
 import { JobQueueService } from '../../job-queue/job-queue.service';
 import { JobQueueService } from '../../job-queue/job-queue.service';
-import { WorkerService } from '../../worker/worker.service';
 import { ConfigArgService } from '../helpers/config-arg/config-arg.service';
 import { ConfigArgService } from '../helpers/config-arg/config-arg.service';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
@@ -40,12 +38,13 @@ import { TranslatableSaver } from '../helpers/translatable-saver/translatable-sa
 import { moveToIndex } from '../helpers/utils/move-to-index';
 import { moveToIndex } from '../helpers/utils/move-to-index';
 import { translateDeep } from '../helpers/utils/translate-entity';
 import { translateDeep } from '../helpers/utils/translate-entity';
 import { TransactionalConnection } from '../transaction/transactional-connection';
 import { TransactionalConnection } from '../transaction/transactional-connection';
-import { ApplyCollectionFiltersJobData, ApplyCollectionFiltersMessage } from '../types/collection-messages';
 
 
 import { AssetService } from './asset.service';
 import { AssetService } from './asset.service';
 import { ChannelService } from './channel.service';
 import { ChannelService } from './channel.service';
 import { FacetValueService } from './facet-value.service';
 import { FacetValueService } from './facet-value.service';
 
 
+type ApplyCollectionFiltersJobData = { ctx: SerializedRequestContext; collectionIds: ID[] };
+
 @Injectable()
 @Injectable()
 export class CollectionService implements OnModuleInit {
 export class CollectionService implements OnModuleInit {
     private rootCollection: Collection | undefined;
     private rootCollection: Collection | undefined;
@@ -59,7 +58,6 @@ export class CollectionService implements OnModuleInit {
         private listQueryBuilder: ListQueryBuilder,
         private listQueryBuilder: ListQueryBuilder,
         private translatableSaver: TranslatableSaver,
         private translatableSaver: TranslatableSaver,
         private eventBus: EventBus,
         private eventBus: EventBus,
-        private workerService: WorkerService,
         private jobQueueService: JobQueueService,
         private jobQueueService: JobQueueService,
         private configService: ConfigService,
         private configService: ConfigService,
         private slugValidator: SlugValidator,
         private slugValidator: SlugValidator,
@@ -67,7 +65,7 @@ export class CollectionService implements OnModuleInit {
         private customFieldRelationService: CustomFieldRelationService,
         private customFieldRelationService: CustomFieldRelationService,
     ) {}
     ) {}
 
 
-    onModuleInit() {
+    async onModuleInit() {
         const productEvents$ = this.eventBus.ofType(ProductEvent);
         const productEvents$ = this.eventBus.ofType(ProductEvent);
         const variantEvents$ = this.eventBus.ofType(ProductVariantEvent);
         const variantEvents$ = this.eventBus.ofType(ProductVariantEvent);
 
 
@@ -81,13 +79,25 @@ export class CollectionService implements OnModuleInit {
                 });
                 });
             });
             });
 
 
-        this.applyFiltersQueue = this.jobQueueService.createQueue({
+        this.applyFiltersQueue = await this.jobQueueService.createQueue({
             name: 'apply-collection-filters',
             name: 'apply-collection-filters',
             process: async job => {
             process: async job => {
+                const ctx = RequestContext.deserialize(job.data.ctx);
+
+                Logger.verbose(`Processing ${job.data.collectionIds.length} Collections`);
                 const collections = await this.connection
                 const collections = await this.connection
                     .getRepository(Collection)
                     .getRepository(Collection)
-                    .findByIds(job.data.collectionIds);
-                return this.applyCollectionFilters(job.data.ctx, collections, job);
+                    .findByIds(job.data.collectionIds, {
+                        relations: ['productVariants'],
+                    });
+                let completed = 0;
+                for (const collection of collections) {
+                    const affectedVariantIds = await this.applyCollectionFiltersInternal(collection);
+                    job.setProgress(Math.ceil((++completed / job.data.collectionIds.length) * 100));
+                    this.eventBus.publish(
+                        new CollectionModificationEvent(ctx, collection, affectedVariantIds),
+                    );
+                }
             },
             },
         });
         });
     }
     }
@@ -393,37 +403,64 @@ export class CollectionService implements OnModuleInit {
     }
     }
 
 
     /**
     /**
-     * Applies the CollectionFilters and returns an array of all affected ProductVariant ids.
+     * Applies the CollectionFilters
      */
      */
-    private async applyCollectionFilters(
-        ctx: SerializedRequestContext,
-        collections: Collection[],
-        job: Job<ApplyCollectionFiltersJobData>,
-    ): Promise<void> {
-        const collectionIds = collections.map(c => c.id);
-        const requestContext = RequestContext.deserialize(ctx);
-
-        return new Promise<void>((resolve, reject) => {
-            this.workerService.send(new ApplyCollectionFiltersMessage({ collectionIds })).subscribe({
-                next: ({ total, completed, duration, collectionId, affectedVariantIds }) => {
-                    const progress = Math.ceil((completed / total) * 100);
-                    const collection = collections.find(c => idsAreEqual(c.id, collectionId));
-                    if (collection) {
-                        this.eventBus.publish(
-                            new CollectionModificationEvent(requestContext, collection, affectedVariantIds),
-                        );
-                    }
-                    job.setProgress(progress);
-                },
-                complete: () => {
-                    resolve();
-                },
-                error: err => {
-                    Logger.error(err);
-                    reject(err);
-                },
-            });
-        });
+    private async applyCollectionFiltersInternal(collection: Collection): Promise<ID[]> {
+        const ancestorFilters = await this.getAncestors(collection.id).then(ancestors =>
+            ancestors.reduce(
+                (filters, c) => [...filters, ...(c.filters || [])],
+                [] as ConfigurableOperation[],
+            ),
+        );
+        const preIds = await this.getCollectionProductVariantIds(collection);
+        collection.productVariants = await this.getFilteredProductVariants([
+            ...ancestorFilters,
+            ...(collection.filters || []),
+        ]);
+        const postIds = collection.productVariants.map(v => v.id);
+        try {
+            await this.connection
+                .getRepository(Collection)
+                // Only update the exact changed properties, to avoid VERY hard-to-debug
+                // non-deterministic race conditions e.g. when the "position" is changed
+                // by moving a Collection and then this save operation clobbers it back
+                // to the old value.
+                .save(pick(collection, ['id', 'productVariants']), {
+                    chunk: Math.ceil(collection.productVariants.length / 500),
+                    reload: false,
+                });
+        } catch (e) {
+            Logger.error(e);
+        }
+        const preIdsSet = new Set(preIds);
+        const postIdsSet = new Set(postIds);
+        const difference = [
+            ...preIds.filter(id => !postIdsSet.has(id)),
+            ...postIds.filter(id => !preIdsSet.has(id)),
+        ];
+        return difference;
+    }
+
+    /**
+     * Applies the CollectionFilters and returns an array of ProductVariant entities which match.
+     */
+    private async getFilteredProductVariants(filters: ConfigurableOperation[]): Promise<ProductVariant[]> {
+        if (filters.length === 0) {
+            return [];
+        }
+        const { collectionFilters } = this.configService.catalogOptions;
+        let qb = this.connection.getRepository(ProductVariant).createQueryBuilder('productVariant');
+
+        for (const filterType of collectionFilters) {
+            const filtersOfType = filters.filter(f => f.code === filterType.code);
+            if (filtersOfType.length) {
+                for (const filter of filtersOfType) {
+                    qb = filterType.apply(qb, filter.args);
+                }
+            }
+        }
+
+        return qb.getMany();
     }
     }
 
 
     /**
     /**

+ 1 - 1
packages/core/src/service/services/order-testing.service.ts

@@ -118,7 +118,7 @@ export class OrderTestingService {
                 line.productVariantId,
                 line.productVariantId,
                 { relations: ['taxCategory'] },
                 { relations: ['taxCategory'] },
             );
             );
-            this.productVariantService.applyChannelPriceAndTax(productVariant, ctx);
+            await this.productVariantService.applyChannelPriceAndTax(productVariant, ctx);
             const orderLine = new OrderLine({
             const orderLine = new OrderLine({
                 productVariant,
                 productVariant,
                 items: [],
                 items: [],

+ 3 - 3
packages/core/src/service/services/order.service.ts

@@ -204,12 +204,12 @@ export class OrderService {
             .addOrderBy('items.createdAt', 'ASC')
             .addOrderBy('items.createdAt', 'ASC')
             .getOne();
             .getOne();
         if (order) {
         if (order) {
-            order.lines.forEach(line => {
+            for (const line of order.lines) {
                 line.productVariant = translateDeep(
                 line.productVariant = translateDeep(
-                    this.productVariantService.applyChannelPriceAndTax(line.productVariant, ctx),
+                    await this.productVariantService.applyChannelPriceAndTax(line.productVariant, ctx),
                     ctx.languageCode,
                     ctx.languageCode,
                 );
                 );
-            });
+            }
             return order;
             return order;
         }
         }
     }
     }

+ 37 - 27
packages/core/src/service/services/product-variant.service.ts

@@ -78,8 +78,10 @@ export class ProductVariantService {
             })
             })
             .getManyAndCount()
             .getManyAndCount()
             .then(async ([variants, totalItems]) => {
             .then(async ([variants, totalItems]) => {
-                const items = variants.map(variant =>
-                    translateDeep(this.applyChannelPriceAndTax(variant, ctx), ctx.languageCode),
+                const items = await Promise.all(
+                    variants.map(async variant =>
+                        translateDeep(await this.applyChannelPriceAndTax(variant, ctx), ctx.languageCode),
+                    ),
                 );
                 );
                 return {
                 return {
                     items,
                     items,
@@ -92,9 +94,9 @@ export class ProductVariantService {
         const relations = ['product', 'product.featuredAsset', 'taxCategory'];
         const relations = ['product', 'product.featuredAsset', 'taxCategory'];
         return this.connection
         return this.connection
             .findOneInChannel(ctx, ProductVariant, productVariantId, ctx.channelId, { relations })
             .findOneInChannel(ctx, ProductVariant, productVariantId, ctx.channelId, { relations })
-            .then(result => {
+            .then(async result => {
                 if (result) {
                 if (result) {
-                    return translateDeep(this.applyChannelPriceAndTax(result, ctx), ctx.languageCode, [
+                    return translateDeep(await this.applyChannelPriceAndTax(result, ctx), ctx.languageCode, [
                         'product',
                         'product',
                     ]);
                     ]);
                 }
                 }
@@ -114,12 +116,14 @@ export class ProductVariantService {
                 ],
                 ],
             })
             })
             .then(variants => {
             .then(variants => {
-                return variants.map(variant =>
-                    translateDeep(this.applyChannelPriceAndTax(variant, ctx), ctx.languageCode, [
-                        'options',
-                        'facetValues',
-                        ['facetValues', 'facet'],
-                    ]),
+                return Promise.all(
+                    variants.map(async variant =>
+                        translateDeep(await this.applyChannelPriceAndTax(variant, ctx), ctx.languageCode, [
+                            'options',
+                            'facetValues',
+                            ['facetValues', 'facet'],
+                        ]),
+                    ),
                 );
                 );
             });
             });
     }
     }
@@ -147,15 +151,18 @@ export class ProductVariantService {
             .andWhere('productVariant.deletedAt IS NULL')
             .andWhere('productVariant.deletedAt IS NULL')
             .orderBy('productVariant.id', 'ASC')
             .orderBy('productVariant.id', 'ASC')
             .getMany()
             .getMany()
-            .then(variants =>
-                variants.map(variant => {
-                    const variantWithPrices = this.applyChannelPriceAndTax(variant, ctx);
-                    return translateDeep(variantWithPrices, ctx.languageCode, [
-                        'options',
-                        'facetValues',
-                        ['facetValues', 'facet'],
-                    ]);
-                }),
+            .then(
+                async variants =>
+                    await Promise.all(
+                        variants.map(async variant => {
+                            const variantWithPrices = await this.applyChannelPriceAndTax(variant, ctx);
+                            return translateDeep(variantWithPrices, ctx.languageCode, [
+                                'options',
+                                'facetValues',
+                                ['facetValues', 'facet'],
+                            ]);
+                        }),
+                    ),
             );
             );
     }
     }
 
 
@@ -180,10 +187,12 @@ export class ProductVariantService {
         }
         }
 
 
         return qb.getManyAndCount().then(async ([variants, totalItems]) => {
         return qb.getManyAndCount().then(async ([variants, totalItems]) => {
-            const items = variants.map(variant => {
-                const variantWithPrices = this.applyChannelPriceAndTax(variant, ctx);
-                return translateDeep(variantWithPrices, ctx.languageCode);
-            });
+            const items = await Promise.all(
+                variants.map(async variant => {
+                    const variantWithPrices = await this.applyChannelPriceAndTax(variant, ctx);
+                    return translateDeep(variantWithPrices, ctx.languageCode);
+                }),
+            );
             return {
             return {
                 items,
                 items,
                 totalItems,
                 totalItems,
@@ -484,7 +493,7 @@ export class ProductVariantService {
     /**
     /**
      * Populates the `price` field with the price for the specified channel.
      * Populates the `price` field with the price for the specified channel.
      */
      */
-    applyChannelPriceAndTax(variant: ProductVariant, ctx: RequestContext): ProductVariant {
+    async applyChannelPriceAndTax(variant: ProductVariant, ctx: RequestContext): Promise<ProductVariant> {
         const channelPrice = variant.productVariantPrices.find(p => idsAreEqual(p.channelId, ctx.channelId));
         const channelPrice = variant.productVariantPrices.find(p => idsAreEqual(p.channelId, ctx.channelId));
         if (!channelPrice) {
         if (!channelPrice) {
             throw new InternalServerError(`error.no-price-found-for-channel`, {
             throw new InternalServerError(`error.no-price-found-for-channel`, {
@@ -498,13 +507,14 @@ export class ProductVariantService {
         if (!activeTaxZone) {
         if (!activeTaxZone) {
             throw new InternalServerError(`error.no-active-tax-zone`);
             throw new InternalServerError(`error.no-active-tax-zone`);
         }
         }
-        const applicableTaxRate = this.taxRateService.getApplicableTaxRate(
+        const applicableTaxRate = await this.taxRateService.getApplicableTaxRate(
+            ctx,
             activeTaxZone,
             activeTaxZone,
             variant.taxCategory,
             variant.taxCategory,
         );
         );
 
 
         const { productVariantPriceCalculationStrategy } = this.configService.catalogOptions;
         const { productVariantPriceCalculationStrategy } = this.configService.catalogOptions;
-        const { price, priceIncludesTax } = productVariantPriceCalculationStrategy.calculate({
+        const { price, priceIncludesTax } = await productVariantPriceCalculationStrategy.calculate({
             inputPrice: channelPrice.price,
             inputPrice: channelPrice.price,
             taxCategory: variant.taxCategory,
             taxCategory: variant.taxCategory,
             activeTaxZone,
             activeTaxZone,
@@ -535,7 +545,7 @@ export class ProductVariantService {
             .findByIds(input.productVariantIds, { relations: ['taxCategory', 'assets'] });
             .findByIds(input.productVariantIds, { relations: ['taxCategory', 'assets'] });
         const priceFactor = input.priceFactor != null ? input.priceFactor : 1;
         const priceFactor = input.priceFactor != null ? input.priceFactor : 1;
         for (const variant of variants) {
         for (const variant of variants) {
-            this.applyChannelPriceAndTax(variant, ctx);
+            await this.applyChannelPriceAndTax(variant, ctx);
             await this.channelService.assignToChannels(ctx, Product, variant.productId, [input.channelId]);
             await this.channelService.assignToChannels(ctx, Product, variant.productId, [input.channelId]);
             await this.channelService.assignToChannels(ctx, ProductVariant, variant.id, [input.channelId]);
             await this.channelService.assignToChannels(ctx, ProductVariant, variant.id, [input.channelId]);
             await this.createOrUpdateProductVariantPrice(
             await this.createOrUpdateProductVariantPrice(

+ 15 - 21
packages/core/src/service/services/tax-rate.service.ts

@@ -1,4 +1,4 @@
-import { Injectable } from '@nestjs/common';
+import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common';
 import {
 import {
     CreateTaxRateInput,
     CreateTaxRateInput,
     DeletionResponse,
     DeletionResponse,
@@ -8,6 +8,7 @@ import {
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 
 
 import { RequestContext } from '../../api/common/request-context';
 import { RequestContext } from '../../api/common/request-context';
+import { RequestContextCacheService } from '../../cache';
 import { EntityNotFoundError } from '../../common/error/errors';
 import { EntityNotFoundError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound } from '../../common/utils';
 import { assertFound } from '../../common/utils';
@@ -17,19 +18,14 @@ import { TaxRate } from '../../entity/tax-rate/tax-rate.entity';
 import { Zone } from '../../entity/zone/zone.entity';
 import { Zone } from '../../entity/zone/zone.entity';
 import { EventBus } from '../../event-bus/event-bus';
 import { EventBus } from '../../event-bus/event-bus';
 import { TaxRateModificationEvent } from '../../event-bus/events/tax-rate-modification-event';
 import { TaxRateModificationEvent } from '../../event-bus/events/tax-rate-modification-event';
-import { WorkerService } from '../../worker/worker.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { patchEntity } from '../helpers/utils/patch-entity';
 import { patchEntity } from '../helpers/utils/patch-entity';
 import { TransactionalConnection } from '../transaction/transactional-connection';
 import { TransactionalConnection } from '../transaction/transactional-connection';
-import { TaxRateUpdatedMessage } from '../types/tax-rate-messages';
+
+const activeTaxRatesKey = 'active-tax-rates';
 
 
 @Injectable()
 @Injectable()
 export class TaxRateService {
 export class TaxRateService {
-    /**
-     * We cache all active TaxRates to avoid hitting the DB many times
-     * per request.
-     */
-    private activeTaxRates: TaxRate[] = [];
     private readonly defaultTaxRate = new TaxRate({
     private readonly defaultTaxRate = new TaxRate({
         value: 0,
         value: 0,
         enabled: true,
         enabled: true,
@@ -41,13 +37,9 @@ export class TaxRateService {
         private connection: TransactionalConnection,
         private connection: TransactionalConnection,
         private eventBus: EventBus,
         private eventBus: EventBus,
         private listQueryBuilder: ListQueryBuilder,
         private listQueryBuilder: ListQueryBuilder,
-        private workerService: WorkerService,
+        private cacheService: RequestContextCacheService,
     ) {}
     ) {}
 
 
-    async initTaxRates() {
-        return this.updateActiveTaxRates(RequestContext.empty());
-    }
-
     findAll(ctx: RequestContext, options?: ListQueryOptions<TaxRate>): Promise<PaginatedList<TaxRate>> {
     findAll(ctx: RequestContext, options?: ListQueryOptions<TaxRate>): Promise<PaginatedList<TaxRate>> {
         return this.listQueryBuilder
         return this.listQueryBuilder
             .build(TaxRate, options, { relations: ['category', 'zone', 'customerGroup'], ctx })
             .build(TaxRate, options, { relations: ['category', 'zone', 'customerGroup'], ctx })
@@ -77,7 +69,6 @@ export class TaxRateService {
         }
         }
         const newTaxRate = await this.connection.getRepository(ctx, TaxRate).save(taxRate);
         const newTaxRate = await this.connection.getRepository(ctx, TaxRate).save(taxRate);
         await this.updateActiveTaxRates(ctx);
         await this.updateActiveTaxRates(ctx);
-        await this.workerService.send(new TaxRateUpdatedMessage(newTaxRate.id)).toPromise();
         this.eventBus.publish(new TaxRateModificationEvent(ctx, newTaxRate));
         this.eventBus.publish(new TaxRateModificationEvent(ctx, newTaxRate));
         return assertFound(this.findOne(ctx, newTaxRate.id));
         return assertFound(this.findOne(ctx, newTaxRate.id));
     }
     }
@@ -111,7 +102,6 @@ export class TaxRateService {
         // Commit the transaction so that the worker process can access the updated
         // Commit the transaction so that the worker process can access the updated
         // TaxRate when updating its own tax rate cache.
         // TaxRate when updating its own tax rate cache.
         await this.connection.commitOpenTransaction(ctx);
         await this.connection.commitOpenTransaction(ctx);
-        await this.workerService.send(new TaxRateUpdatedMessage(updatedTaxRate.id)).toPromise();
 
 
         this.eventBus.publish(new TaxRateModificationEvent(ctx, updatedTaxRate));
         this.eventBus.publish(new TaxRateModificationEvent(ctx, updatedTaxRate));
         return assertFound(this.findOne(ctx, taxRate.id));
         return assertFound(this.findOne(ctx, taxRate.id));
@@ -132,17 +122,21 @@ export class TaxRateService {
         }
         }
     }
     }
 
 
-    getActiveTaxRates(): TaxRate[] {
-        return this.activeTaxRates;
+    async getApplicableTaxRate(ctx: RequestContext, zone: Zone, taxCategory: TaxCategory): Promise<TaxRate> {
+        const rate = (await this.getActiveTaxRates(ctx)).find(r => r.test(zone, taxCategory));
+        return rate || this.defaultTaxRate;
     }
     }
 
 
-    getApplicableTaxRate(zone: Zone, taxCategory: TaxCategory): TaxRate {
-        const rate = this.getActiveTaxRates().find(r => r.test(zone, taxCategory));
-        return rate || this.defaultTaxRate;
+    async getActiveTaxRates(ctx: RequestContext): Promise<TaxRate[]> {
+        return this.cacheService.get(ctx, activeTaxRatesKey, () => this.findActiveTaxRates(ctx));
     }
     }
 
 
     async updateActiveTaxRates(ctx: RequestContext) {
     async updateActiveTaxRates(ctx: RequestContext) {
-        this.activeTaxRates = await this.connection.getRepository(ctx, TaxRate).find({
+        await this.cacheService.set(ctx, activeTaxRatesKey, await this.findActiveTaxRates(ctx));
+    }
+
+    private async findActiveTaxRates(ctx: RequestContext): Promise<TaxRate[]> {
+        return await this.connection.getRepository(ctx, TaxRate).find({
             relations: ['category', 'zone', 'customerGroup'],
             relations: ['category', 'zone', 'customerGroup'],
             where: {
             where: {
                 enabled: true,
                 enabled: true,

+ 0 - 21
packages/core/src/service/types/collection-messages.ts

@@ -1,21 +0,0 @@
-import { ID } from '@vendure/common/lib/shared-types';
-
-import { SerializedRequestContext } from '../../api/common/request-context';
-import { WorkerMessage } from '../../worker/types';
-
-export interface ProcessCollectionsResponse {
-    total: number;
-    completed: number;
-    duration: number;
-    collectionId: ID;
-    affectedVariantIds: ID[];
-}
-
-export class ApplyCollectionFiltersMessage extends WorkerMessage<
-    { collectionIds: ID[] },
-    ProcessCollectionsResponse
-> {
-    static readonly pattern = 'ApplyCollectionFilters';
-}
-
-export type ApplyCollectionFiltersJobData = { ctx: SerializedRequestContext; collectionIds: ID[] };

+ 0 - 10
packages/core/src/service/types/tax-rate-messages.ts

@@ -1,10 +0,0 @@
-import { ID } from '@vendure/common/lib/shared-types';
-
-import { WorkerMessage } from '../../worker/types';
-
-/**
- * A message sent to the Worker whenever a TaxRate is updated.
- */
-export class TaxRateUpdatedMessage extends WorkerMessage<ID, boolean> {
-    static readonly pattern = 'TaxRateUpdated';
-}

+ 1 - 1
packages/core/src/testing/order-test-utils.ts

@@ -124,7 +124,7 @@ export class MockTaxRateService {
         /* noop */
         /* noop */
     }
     }
 
 
-    getApplicableTaxRate(zone: Zone, taxCategory: TaxCategory): TaxRate {
+    async getApplicableTaxRate(ctx: RequestContext, zone: Zone, taxCategory: TaxCategory): Promise<TaxRate> {
         const rate = this.activeTaxRates.find(r => r.test(zone, taxCategory));
         const rate = this.activeTaxRates.find(r => r.test(zone, taxCategory));
         return rate || taxRateDefaultStandard;
         return rate || taxRateDefaultStandard;
     }
     }

+ 0 - 1
packages/core/src/worker/constants.ts

@@ -1 +0,0 @@
-export const VENDURE_WORKER_CLIENT = Symbol('VENDURE_WORKER_CLIENT');

+ 0 - 3
packages/core/src/worker/index.ts

@@ -1,3 +0,0 @@
-export * from './async-observable';
-export * from './worker.service';
-export * from './types';

+ 0 - 25
packages/core/src/worker/message-interceptor.ts

@@ -1,25 +0,0 @@
-import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
-import { Observable } from 'rxjs';
-import { finalize, tap } from 'rxjs/operators';
-
-import { WorkerMonitor } from './worker-monitor';
-
-/**
- * This interceptor is used to keep track of open worker tasks, so that the WorkerModule
- * is not allowed to be destroyed while tasks are in progress.
- */
-@Injectable()
-export class MessageInterceptor implements NestInterceptor {
-    constructor(private monitor: WorkerMonitor) {}
-
-    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
-        this.monitor.increment();
-        return next
-            .handle()
-            .pipe(
-                finalize(() => {
-                    this.monitor.decrement();
-                }),
-            );
-    }
-}

+ 0 - 44
packages/core/src/worker/types.ts

@@ -1,44 +0,0 @@
-/**
- * @description
- * A class which is used to define the contract between the Vendure server and the worker process. Used
- * by the {@link WorkerService} `send` method.
- *
- * @example
- * ```TypeScript
- * export class ReindexMessage extends WorkerMessage<{ ctx: SerializedRequestContext }, boolean> {
- *   static readonly pattern = 'Reindex';
- * }
- *
- * // in a Service running on the main process
- * class MyService {
- *
- *   constructor(private workerService: WorkerService) {}
- *
- *   reindex(ctx: RequestContext): Observable<boolean>> {
- *     // If you need to send the RequestContext object to the worker,
- *     // be sure to send a serialized version to avoid errors.
- *     const serializedCtx = ctx.serialize();
- *     return this.workerService.send(new ReindexMessage({ ctx: serializedCtx }))
- *   }
- * }
- *
- * // in a microservice Controller on the worker process
- * class MyController {
- *
- *  \@MessagePattern(ReindexMessage.pattern)
- *  reindex({ ctx: serializedCtx }: ReindexMessage['data']): Observable<ReindexMessage['response']> {
- *    // Convert the SerializedRequestContext back into a regular
- *    // RequestContext object
- *    const ctx = RequestContext.deserialize(serializedCtx);
- *    // ... some long-running workload
- *  }
- * }
- * ```
- *
- * @docsCategory worker
- */
-export abstract class WorkerMessage<T, R> {
-    static readonly pattern: string;
-    constructor(public data: T) {}
-    response: R;
-}

+ 0 - 37
packages/core/src/worker/worker-monitor.ts

@@ -1,37 +0,0 @@
-import { Injectable } from '@nestjs/common';
-import { BehaviorSubject } from 'rxjs';
-import { debounceTime, takeWhile, tap } from 'rxjs/operators';
-
-import { Logger } from '../config/logger/vendure-logger';
-
-/**
- * This service is responsible for keeping track of incomplete worker tasks
- * to ensure that the WorkerModule is not destroyed before active tasks complete.
- */
-@Injectable()
-export class WorkerMonitor {
-    openTasks = new BehaviorSubject<number>(0);
-    get openTaskCount(): number {
-        return this.openTasks.value;
-    }
-    increment() {
-        this.openTasks.next(this.openTasks.value + 1);
-    }
-    decrement() {
-        this.openTasks.next(this.openTasks.value - 1);
-    }
-    waitForOpenTasksToComplete(): Promise<number> {
-        if (0 < this.openTaskCount) {
-            Logger.info('Waiting for open worker tasks to complete...');
-        }
-        return this.openTasks.asObservable().pipe(
-            tap(count => {
-                if (0 < count) {
-                    Logger.info(`${count} tasks open`);
-                }
-            }),
-            debounceTime(100),
-            takeWhile(value => value > 0),
-        ).toPromise();
-    }
-}

+ 0 - 27
packages/core/src/worker/worker-service.module.ts

@@ -1,27 +0,0 @@
-import { Module } from '@nestjs/common';
-import { ClientProxyFactory } from '@nestjs/microservices';
-
-import { ConfigModule } from '../config/config.module';
-import { ConfigService } from '../config/config.service';
-
-import { VENDURE_WORKER_CLIENT } from './constants';
-import { WorkerService } from './worker.service';
-
-@Module({
-    imports: [ConfigModule],
-    providers: [
-        WorkerService,
-        {
-            provide: VENDURE_WORKER_CLIENT,
-            useFactory: (configService: ConfigService) => {
-                return ClientProxyFactory.create({
-                    transport: configService.workerOptions.transport as any,
-                    options: configService.workerOptions.options as any,
-                });
-            },
-            inject: [ConfigService],
-        },
-    ],
-    exports: [WorkerService],
-})
-export class WorkerServiceModule {}

+ 0 - 41
packages/core/src/worker/worker.module.ts

@@ -1,41 +0,0 @@
-import { Module, OnApplicationShutdown, OnModuleDestroy } from '@nestjs/common';
-import { APP_INTERCEPTOR } from '@nestjs/core';
-
-import { ConfigModule } from '../config/config.module';
-import { Logger } from '../config/logger/vendure-logger';
-import { PluginModule } from '../plugin/plugin.module';
-import { ProcessContextModule } from '../process-context/process-context.module';
-import { ServiceModule } from '../service/service.module';
-
-import { MessageInterceptor } from './message-interceptor';
-import { WorkerMonitor } from './worker-monitor';
-import { WorkerServiceModule } from './worker-service.module';
-
-@Module({
-    imports: [
-        ConfigModule,
-        ServiceModule.forWorker(),
-        PluginModule.forWorker(),
-        WorkerServiceModule,
-        ProcessContextModule.forWorker(),
-    ],
-    providers: [
-        WorkerMonitor,
-        {
-            provide: APP_INTERCEPTOR,
-            useClass: MessageInterceptor,
-        },
-    ],
-})
-export class WorkerModule implements OnModuleDestroy, OnApplicationShutdown {
-    constructor(private monitor: WorkerMonitor) {}
-    onModuleDestroy() {
-        return this.monitor.waitForOpenTasksToComplete();
-    }
-
-    onApplicationShutdown(signal?: string) {
-        if (signal) {
-            Logger.info('Worker Received shutdown signal:' + signal);
-        }
-    }
-}

+ 0 - 62
packages/core/src/worker/worker.service.ts

@@ -1,62 +0,0 @@
-import { Inject, Injectable, OnModuleDestroy } from '@nestjs/common';
-import { ClientProxy } from '@nestjs/microservices';
-import { BehaviorSubject, Observable } from 'rxjs';
-import { filter, finalize, mergeMap, take } from 'rxjs/operators';
-
-import { VENDURE_WORKER_CLIENT } from './constants';
-import { WorkerMessage } from './types';
-
-/**
- * @description
- * This service is responsible for sending messages to the Worker process. See the {@link WorkerMessage}
- * docs for an example of usage.
- *
- * @docsCategory worker
- */
-@Injectable()
-export class WorkerService implements OnModuleDestroy {
-    private pendingConnection = false;
-    private initialConnection = new BehaviorSubject(false);
-    constructor(@Inject(VENDURE_WORKER_CLIENT) private readonly client: ClientProxy) {}
-
-    /**
-     * @description
-     * Sends a {@link WorkerMessage} to the worker process, where there should be a Controller with a method
-     * listening out for the message's pattern.
-     */
-    send<T, R>(message: WorkerMessage<T, R>): Observable<R> {
-        // The rather convoluted logic here is required in order to prevent more than
-        // one connection being opened in the event that the `send` method is called multiple
-        // times in the same event loop tick.
-        // On the first invocation, the first path is taken, which establishes the single
-        // connection (implicit in the first call to ClientProxy.send()). All subsequent
-        // invocations take the second code path.
-        if (!this.pendingConnection && this.initialConnection.value === false) {
-            this.pendingConnection = true;
-            return this.client
-                .send<R, T>((message.constructor as typeof WorkerMessage).pattern, message.data)
-                .pipe(
-                    finalize(() => {
-                        this.initialConnection.next(true);
-                        this.pendingConnection = false;
-                    }),
-                );
-        } else {
-            return this.initialConnection.pipe(
-                filter((val) => val),
-                take(1),
-                mergeMap(() => {
-                    return this.client.send<R, T>(
-                        (message.constructor as typeof WorkerMessage).pattern,
-                        message.data,
-                    );
-                }),
-            );
-        }
-    }
-
-    /** @internal */
-    onModuleDestroy() {
-        this.client.close();
-    }
-}

+ 5 - 1
packages/create/templates/index-worker.hbs

@@ -1,7 +1,11 @@
 {{#if isTs }}import { bootstrapWorker } from '@vendure/core';{{else}}const { bootstrapWorker } = require('@vendure/core');{{/if}}
 {{#if isTs }}import { bootstrapWorker } from '@vendure/core';{{else}}const { bootstrapWorker } = require('@vendure/core');{{/if}}
 {{#if isTs }}import { config } from './vendure-config';{{else}}const { config } = require('./vendure-config');{{/if}}
 {{#if isTs }}import { config } from './vendure-config';{{else}}const { config } = require('./vendure-config');{{/if}}
 
 
-bootstrapWorker(config).catch(err => {
+bootstrapWorker(config)
+.then(await app => {
+  await app.get(JobQueueService).start();
+})
+.catch(err => {
     // tslint:disable-next-line:no-console
     // tslint:disable-next-line:no-console
     console.log(err);
     console.log(err);
 });
 });

+ 5 - 3
packages/create/templates/vendure-config.hbs

@@ -78,14 +78,13 @@ const path = require('path');
         AssetServerPlugin.init({
         AssetServerPlugin.init({
             route: 'assets',
             route: 'assets',
             assetUploadDir: path.join(__dirname, '../static/assets'),
             assetUploadDir: path.join(__dirname, '../static/assets'),
-            port: 3001,
         }),
         }),
         DefaultJobQueuePlugin,
         DefaultJobQueuePlugin,
         DefaultSearchPlugin,
         DefaultSearchPlugin,
         EmailPlugin.init({
         EmailPlugin.init({
             devMode: true,
             devMode: true,
             outputPath: path.join(__dirname, '../static/email/test-emails'),
             outputPath: path.join(__dirname, '../static/email/test-emails'),
-            mailboxPort: 3003,
+            route: 'mailbox',
             handlers: defaultEmailHandlers,
             handlers: defaultEmailHandlers,
             templatePath: path.join(__dirname, '../static/email/templates'),
             templatePath: path.join(__dirname, '../static/email/templates'),
             globalTemplateVars: {
             globalTemplateVars: {
@@ -96,7 +95,10 @@ const path = require('path');
                 changeEmailAddressUrl: 'http://localhost:8080/verify-email-address-change'
                 changeEmailAddressUrl: 'http://localhost:8080/verify-email-address-change'
             },
             },
         }),
         }),
-        AdminUiPlugin.init({ port: 3002 }),
+        AdminUiPlugin.init({
+            route: 'admin',
+            port: 3002,
+        }),
     ],
     ],
 };
 };
 {{#if isTs}}
 {{#if isTs}}

+ 8 - 7
packages/dev-server/dev-config.ts

@@ -15,6 +15,7 @@ import {
     PaymentMethodEligibilityChecker,
     PaymentMethodEligibilityChecker,
     VendureConfig,
     VendureConfig,
 } from '@vendure/core';
 } from '@vendure/core';
+import { ElasticsearchPlugin } from '@vendure/elasticsearch-plugin';
 import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
 import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
 import path from 'path';
 import path from 'path';
 import { ConnectionOptions } from 'typeorm';
 import { ConnectionOptions } from 'typeorm';
@@ -78,20 +79,19 @@ export const devConfig: VendureConfig = {
         AssetServerPlugin.init({
         AssetServerPlugin.init({
             route: 'assets',
             route: 'assets',
             assetUploadDir: path.join(__dirname, 'assets'),
             assetUploadDir: path.join(__dirname, 'assets'),
-            port: 5002,
         }),
         }),
-        DefaultSearchPlugin,
+        // DefaultSearchPlugin,
         DefaultJobQueuePlugin,
         DefaultJobQueuePlugin,
-        // ElasticsearchPlugin.init({
-        //     host: 'http://localhost',
-        //     port: 9200,
-        // }),
+        ElasticsearchPlugin.init({
+            host: 'http://localhost',
+            port: 9200,
+        }),
         EmailPlugin.init({
         EmailPlugin.init({
             devMode: true,
             devMode: true,
+            route: 'mailbox',
             handlers: defaultEmailHandlers,
             handlers: defaultEmailHandlers,
             templatePath: path.join(__dirname, '../email-plugin/templates'),
             templatePath: path.join(__dirname, '../email-plugin/templates'),
             outputPath: path.join(__dirname, 'test-emails'),
             outputPath: path.join(__dirname, 'test-emails'),
-            mailboxPort: 5003,
             globalTemplateVars: {
             globalTemplateVars: {
                 verifyEmailAddressUrl: 'http://localhost:4201/verify',
                 verifyEmailAddressUrl: 'http://localhost:4201/verify',
                 passwordResetUrl: 'http://localhost:4201/reset-password',
                 passwordResetUrl: 'http://localhost:4201/reset-password',
@@ -99,6 +99,7 @@ export const devConfig: VendureConfig = {
             },
             },
         }),
         }),
         AdminUiPlugin.init({
         AdminUiPlugin.init({
+            route: 'admin',
             port: 5001,
             port: 5001,
         }),
         }),
     ],
     ],

+ 10 - 6
packages/dev-server/index-worker.ts

@@ -1,4 +1,4 @@
-import { bootstrapWorker } from '@vendure/core';
+import { bootstrapWorker, JobQueueService } from '@vendure/core';
 
 
 import { devConfig } from './dev-config';
 import { devConfig } from './dev-config';
 
 
@@ -6,8 +6,12 @@ import { devConfig } from './dev-config';
 // fix race condition when modifying DB
 // fix race condition when modifying DB
 devConfig.dbConnectionOptions = { ...devConfig.dbConnectionOptions, synchronize: false };
 devConfig.dbConnectionOptions = { ...devConfig.dbConnectionOptions, synchronize: false };
 
 
-bootstrapWorker(devConfig).catch(err => {
-    // tslint:disable-next-line
-    console.log(err);
-    process.exit(1);
-});
+bootstrapWorker(devConfig)
+    .then(app => {
+        app.get(JobQueueService).start();
+    })
+    .catch(err => {
+        // tslint:disable-next-line
+        console.log(err);
+        process.exit(1);
+    });

+ 12 - 6
packages/dev-server/index.ts

@@ -1,12 +1,18 @@
-import { bootstrap } from '@vendure/core';
+import { bootstrap, JobQueueService } from '@vendure/core';
 
 
 import { devConfig } from './dev-config';
 import { devConfig } from './dev-config';
 
 
 /**
 /**
  * This bootstraps the dev server, used for testing Vendure during development.
  * This bootstraps the dev server, used for testing Vendure during development.
  */
  */
-bootstrap(devConfig).catch(err => {
-    // tslint:disable-next-line
-    console.log(err);
-    process.exit(1);
-});
+bootstrap(devConfig)
+    .then(app => {
+        if (process.env.RUN_JOB_QUEUE) {
+            app.get(JobQueueService).start();
+        }
+    })
+    .catch(err => {
+        // tslint:disable-next-line
+        console.log(err);
+        process.exit(1);
+    });

+ 0 - 3
packages/dev-server/populate-dev-server.ts

@@ -26,9 +26,6 @@ if (require.main === module) {
             importExportOptions: {
             importExportOptions: {
                 importAssetsDir: path.join(__dirname, '../core/mock-data/assets'),
                 importAssetsDir: path.join(__dirname, '../core/mock-data/assets'),
             },
             },
-            workerOptions: {
-                runInMainProcess: true,
-            },
             customFields: {},
             customFields: {},
         }),
         }),
     );
     );

+ 7 - 25
packages/dev-server/test-plugins/google-auth/google-auth-plugin.ts

@@ -1,7 +1,6 @@
-import { INestApplication } from '@nestjs/common';
-import { OnVendureBootstrap, OnVendureClose, PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { MiddlewareConsumer, NestModule, OnApplicationBootstrap, OnModuleDestroy } from '@nestjs/common';
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
 import express from 'express';
 import express from 'express';
-import { Server } from 'http';
 import path from 'path';
 import path from 'path';
 
 
 import { GoogleAuthenticationStrategy } from './google-authentication-strategy';
 import { GoogleAuthenticationStrategy } from './google-authentication-strategy';
@@ -17,14 +16,14 @@ export type GoogleAuthPluginOptions = {
  *
  *
  * Then add this plugin to the dev config.
  * Then add this plugin to the dev config.
  *
  *
- * The "storefront" is a simple html file which is served on http://localhost:80,
+ * The "storefront" is a simple html file which is served on http://localhost:3000/google-login,
  * but to get it to work with the Google login button you'll need to resolve it to some
  * but to get it to work with the Google login button you'll need to resolve it to some
  * public-looking url such as `http://google-login-test.com` by modifying your OS
  * public-looking url such as `http://google-login-test.com` by modifying your OS
  * hosts file.
  * hosts file.
  */
  */
 @VendurePlugin({
 @VendurePlugin({
     imports: [PluginCommonModule],
     imports: [PluginCommonModule],
-    configuration: (config) => {
+    configuration: config => {
         config.authOptions.shopAuthenticationStrategy = [
         config.authOptions.shopAuthenticationStrategy = [
             ...config.authOptions.shopAuthenticationStrategy,
             ...config.authOptions.shopAuthenticationStrategy,
             new GoogleAuthenticationStrategy(GoogleAuthPlugin.options.clientId),
             new GoogleAuthenticationStrategy(GoogleAuthPlugin.options.clientId),
@@ -32,32 +31,15 @@ export type GoogleAuthPluginOptions = {
         return config;
         return config;
     },
     },
 })
 })
-export class GoogleAuthPlugin implements OnVendureBootstrap, OnVendureClose {
+export class GoogleAuthPlugin implements NestModule {
     static options: GoogleAuthPluginOptions;
     static options: GoogleAuthPluginOptions;
-    private staticServer: Server;
 
 
     static init(options: GoogleAuthPluginOptions) {
     static init(options: GoogleAuthPluginOptions) {
         this.options = options;
         this.options = options;
         return GoogleAuthPlugin;
         return GoogleAuthPlugin;
     }
     }
 
 
-    onVendureBootstrap() {
-        // Set up a static express server to serve the demo login page
-        // from public/index.html.
-        const app = express();
-        app.use(express.static(path.join(__dirname, 'public')));
-        this.staticServer = app.listen(80);
-    }
-
-    onVendureClose(): void | Promise<void> {
-        return new Promise((resolve, reject) => {
-            this.staticServer.close((err) => {
-                if (err) {
-                    reject(err);
-                } else {
-                    resolve();
-                }
-            });
-        });
+    configure(consumer: MiddlewareConsumer) {
+        consumer.apply(express.static(path.join(__dirname, 'public'))).forRoutes('google-login');
     }
     }
 }
 }

+ 6 - 39
packages/dev-server/test-plugins/keycloak-auth/keycloak-auth-plugin.ts

@@ -1,12 +1,6 @@
-import {
-    createProxyHandler,
-    OnVendureBootstrap,
-    OnVendureClose,
-    PluginCommonModule,
-    VendurePlugin,
-} from '@vendure/core';
+import { MiddlewareConsumer, NestModule } from '@nestjs/common';
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
 import express from 'express';
 import express from 'express';
-import { Server } from 'http';
 import path from 'path';
 import path from 'path';
 
 
 import { KeycloakAuthenticationStrategy } from './keycloak-authentication-strategy';
 import { KeycloakAuthenticationStrategy } from './keycloak-authentication-strategy';
@@ -24,43 +18,16 @@ import { KeycloakAuthenticationStrategy } from './keycloak-authentication-strate
  */
  */
 @VendurePlugin({
 @VendurePlugin({
     imports: [PluginCommonModule],
     imports: [PluginCommonModule],
-    configuration: (config) => {
+    configuration: config => {
         config.authOptions.adminAuthenticationStrategy = [
         config.authOptions.adminAuthenticationStrategy = [
             ...config.authOptions.adminAuthenticationStrategy,
             ...config.authOptions.adminAuthenticationStrategy,
             new KeycloakAuthenticationStrategy(),
             new KeycloakAuthenticationStrategy(),
         ];
         ];
-        config.apiOptions.middleware.push({
-            handler: createProxyHandler({
-                port: 3042,
-                route: 'keycloak-login',
-                label: 'Keycloak Login',
-                basePath: '',
-            }),
-            route: 'keycloak-login',
-        });
         return config;
         return config;
     },
     },
 })
 })
-export class KeycloakAuthPlugin implements OnVendureBootstrap, OnVendureClose {
-    private staticServer: Server;
-
-    onVendureBootstrap() {
-        // Set up a static express server to serve the demo login page
-        // from public/index.html.
-        const app = express();
-        app.use(express.static(path.join(__dirname, 'public')));
-        this.staticServer = app.listen(3042);
-    }
-
-    onVendureClose(): void | Promise<void> {
-        return new Promise((resolve, reject) => {
-            this.staticServer.close((err) => {
-                if (err) {
-                    reject(err);
-                } else {
-                    resolve();
-                }
-            });
-        });
+export class KeycloakAuthPlugin implements NestModule {
+    configure(consumer: MiddlewareConsumer) {
+        consumer.apply(express.static(path.join(__dirname, 'public'))).forRoutes('keycloak-login');
     }
     }
 }
 }

+ 0 - 5
packages/elasticsearch-plugin/e2e/elasticsearch-plugin-uuid.e2e-spec.ts

@@ -29,11 +29,6 @@ describe('Elasticsearch plugin with UuidIdStrategy', () => {
             apiOptions: {
             apiOptions: {
                 port: 4050,
                 port: 4050,
             },
             },
-            workerOptions: {
-                options: {
-                    port: 4055,
-                },
-            },
             entityIdStrategy: new UuidIdStrategy(),
             entityIdStrategy: new UuidIdStrategy(),
             logger: new DefaultLogger({ level: LogLevel.Info }),
             logger: new DefaultLogger({ level: LogLevel.Info }),
             plugins: [
             plugins: [

+ 0 - 5
packages/elasticsearch-plugin/e2e/elasticsearch-plugin.e2e-spec.ts

@@ -102,11 +102,6 @@ describe('Elasticsearch plugin', () => {
             apiOptions: {
             apiOptions: {
                 port: 4050,
                 port: 4050,
             },
             },
-            workerOptions: {
-                options: {
-                    port: 4055,
-                },
-            },
             logger: new DefaultLogger({ level: LogLevel.Info }),
             logger: new DefaultLogger({ level: LogLevel.Info }),
             plugins: [
             plugins: [
                 ElasticsearchPlugin.init({
                 ElasticsearchPlugin.init({

+ 42 - 69
packages/elasticsearch-plugin/src/elasticsearch-index.service.ts

@@ -1,4 +1,4 @@
-import { Injectable } from '@nestjs/common';
+import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
 import { assertNever } from '@vendure/common/lib/shared-utils';
 import { assertNever } from '@vendure/common/lib/shared-utils';
 import {
 import {
     Asset,
     Asset,
@@ -10,64 +10,52 @@ import {
     Product,
     Product,
     ProductVariant,
     ProductVariant,
     RequestContext,
     RequestContext,
-    WorkerMessage,
-    WorkerService,
 } from '@vendure/core';
 } from '@vendure/core';
+import { Observable } from 'rxjs';
 
 
-import { ReindexMessageResponse } from './indexer.controller';
-import {
-    AssignProductToChannelMessage,
-    AssignVariantToChannelMessage,
-    DeleteAssetMessage,
-    DeleteProductMessage,
-    DeleteVariantMessage,
-    ReindexMessage,
-    RemoveProductFromChannelMessage,
-    RemoveVariantFromChannelMessage,
-    UpdateAssetMessage,
-    UpdateIndexQueueJobData,
-    UpdateProductMessage,
-    UpdateVariantMessage,
-    UpdateVariantsByIdMessage,
-} from './types';
-
-let updateIndexQueue: JobQueue<UpdateIndexQueueJobData> | undefined;
+import { ElasticsearchIndexerController, ReindexMessageResponse } from './indexer.controller';
+import { UpdateIndexQueueJobData } from './types';
 
 
 @Injectable()
 @Injectable()
-export class ElasticsearchIndexService {
-    constructor(private workerService: WorkerService, private jobService: JobQueueService) {}
+export class ElasticsearchIndexService implements OnApplicationBootstrap {
+    private updateIndexQueue: JobQueue<UpdateIndexQueueJobData>;
+
+    constructor(
+        private jobService: JobQueueService,
+        private indexerController: ElasticsearchIndexerController,
+    ) {}
 
 
-    initJobQueue() {
-        updateIndexQueue = this.jobService.createQueue({
+    async onApplicationBootstrap() {
+        this.updateIndexQueue = await this.jobService.createQueue({
             name: 'update-search-index',
             name: 'update-search-index',
             process: job => {
             process: job => {
                 const data = job.data;
                 const data = job.data;
                 switch (data.type) {
                 switch (data.type) {
                     case 'reindex':
                     case 'reindex':
                         Logger.verbose(`sending ReindexMessage`);
                         Logger.verbose(`sending ReindexMessage`);
-                        return this.sendMessageWithProgress(job, new ReindexMessage(data));
+                        return this.jobWithProgress(job, this.indexerController.reindex(data));
                     case 'update-product':
                     case 'update-product':
-                        return this.sendMessage(job, new UpdateProductMessage(data));
+                        return this.indexerController.updateProduct(data);
                     case 'update-variants':
                     case 'update-variants':
-                        return this.sendMessage(job, new UpdateVariantMessage(data));
+                        return this.indexerController.updateVariants(data);
                     case 'delete-product':
                     case 'delete-product':
-                        return this.sendMessage(job, new DeleteProductMessage(data));
+                        return this.indexerController.deleteProduct(data);
                     case 'delete-variant':
                     case 'delete-variant':
-                        return this.sendMessage(job, new DeleteVariantMessage(data));
+                        return this.indexerController.deleteVariants(data);
                     case 'update-variants-by-id':
                     case 'update-variants-by-id':
-                        return this.sendMessageWithProgress(job, new UpdateVariantsByIdMessage(data));
+                        return this.jobWithProgress(job, this.indexerController.updateVariantsById(data));
                     case 'update-asset':
                     case 'update-asset':
-                        return this.sendMessage(job, new UpdateAssetMessage(data));
+                        return this.indexerController.updateAsset(data);
                     case 'delete-asset':
                     case 'delete-asset':
-                        return this.sendMessage(job, new DeleteAssetMessage(data));
+                        return this.indexerController.deleteAsset(data);
                     case 'assign-product-to-channel':
                     case 'assign-product-to-channel':
-                        return this.sendMessage(job, new AssignProductToChannelMessage(data));
+                        return this.indexerController.assignProductToChannel(data);
                     case 'remove-product-from-channel':
                     case 'remove-product-from-channel':
-                        return this.sendMessage(job, new RemoveProductFromChannelMessage(data));
+                        return this.indexerController.removeProductFromChannel(data);
                     case 'assign-variant-to-channel':
                     case 'assign-variant-to-channel':
-                        return this.sendMessage(job, new AssignVariantToChannelMessage(data));
+                        return this.indexerController.assignVariantToChannel(data);
                     case 'remove-variant-from-channel':
                     case 'remove-variant-from-channel':
-                        return this.sendMessage(job, new RemoveVariantFromChannelMessage(data));
+                        return this.indexerController.removeVariantFromChannel(data);
                     default:
                     default:
                         assertNever(data);
                         assertNever(data);
                         return Promise.resolve();
                         return Promise.resolve();
@@ -77,29 +65,29 @@ export class ElasticsearchIndexService {
     }
     }
 
 
     reindex(ctx: RequestContext, dropIndices: boolean) {
     reindex(ctx: RequestContext, dropIndices: boolean) {
-        return this.addJobToQueue({ type: 'reindex', ctx: ctx.serialize(), dropIndices });
+        return this.updateIndexQueue.add({ type: 'reindex', ctx: ctx.serialize(), dropIndices });
     }
     }
 
 
     updateProduct(ctx: RequestContext, product: Product) {
     updateProduct(ctx: RequestContext, product: Product) {
-        this.addJobToQueue({ type: 'update-product', ctx: ctx.serialize(), productId: product.id });
+        this.updateIndexQueue.add({ type: 'update-product', ctx: ctx.serialize(), productId: product.id });
     }
     }
 
 
     updateVariants(ctx: RequestContext, variants: ProductVariant[]) {
     updateVariants(ctx: RequestContext, variants: ProductVariant[]) {
         const variantIds = variants.map(v => v.id);
         const variantIds = variants.map(v => v.id);
-        this.addJobToQueue({ type: 'update-variants', ctx: ctx.serialize(), variantIds });
+        this.updateIndexQueue.add({ type: 'update-variants', ctx: ctx.serialize(), variantIds });
     }
     }
 
 
     deleteProduct(ctx: RequestContext, product: Product) {
     deleteProduct(ctx: RequestContext, product: Product) {
-        this.addJobToQueue({ type: 'delete-product', ctx: ctx.serialize(), productId: product.id });
+        this.updateIndexQueue.add({ type: 'delete-product', ctx: ctx.serialize(), productId: product.id });
     }
     }
 
 
     deleteVariant(ctx: RequestContext, variants: ProductVariant[]) {
     deleteVariant(ctx: RequestContext, variants: ProductVariant[]) {
         const variantIds = variants.map(v => v.id);
         const variantIds = variants.map(v => v.id);
-        this.addJobToQueue({ type: 'delete-variant', ctx: ctx.serialize(), variantIds });
+        this.updateIndexQueue.add({ type: 'delete-variant', ctx: ctx.serialize(), variantIds });
     }
     }
 
 
     assignProductToChannel(ctx: RequestContext, product: Product, channelId: ID) {
     assignProductToChannel(ctx: RequestContext, product: Product, channelId: ID) {
-        this.addJobToQueue({
+        this.updateIndexQueue.add({
             type: 'assign-product-to-channel',
             type: 'assign-product-to-channel',
             ctx: ctx.serialize(),
             ctx: ctx.serialize(),
             productId: product.id,
             productId: product.id,
@@ -108,7 +96,7 @@ export class ElasticsearchIndexService {
     }
     }
 
 
     removeProductFromChannel(ctx: RequestContext, product: Product, channelId: ID) {
     removeProductFromChannel(ctx: RequestContext, product: Product, channelId: ID) {
-        this.addJobToQueue({
+        this.updateIndexQueue.add({
             type: 'remove-product-from-channel',
             type: 'remove-product-from-channel',
             ctx: ctx.serialize(),
             ctx: ctx.serialize(),
             productId: product.id,
             productId: product.id,
@@ -117,7 +105,7 @@ export class ElasticsearchIndexService {
     }
     }
 
 
     assignVariantToChannel(ctx: RequestContext, productVariantId: ID, channelId: ID) {
     assignVariantToChannel(ctx: RequestContext, productVariantId: ID, channelId: ID) {
-        this.addJobToQueue({
+        this.updateIndexQueue.add({
             type: 'assign-variant-to-channel',
             type: 'assign-variant-to-channel',
             ctx: ctx.serialize(),
             ctx: ctx.serialize(),
             productVariantId,
             productVariantId,
@@ -126,7 +114,7 @@ export class ElasticsearchIndexService {
     }
     }
 
 
     removeVariantFromChannel(ctx: RequestContext, productVariantId: ID, channelId: ID) {
     removeVariantFromChannel(ctx: RequestContext, productVariantId: ID, channelId: ID) {
-        this.addJobToQueue({
+        this.updateIndexQueue.add({
             type: 'remove-variant-from-channel',
             type: 'remove-variant-from-channel',
             ctx: ctx.serialize(),
             ctx: ctx.serialize(),
             productVariantId,
             productVariantId,
@@ -135,41 +123,26 @@ export class ElasticsearchIndexService {
     }
     }
 
 
     updateVariantsById(ctx: RequestContext, ids: ID[]) {
     updateVariantsById(ctx: RequestContext, ids: ID[]) {
-        this.addJobToQueue({ type: 'update-variants-by-id', ctx: ctx.serialize(), ids });
+        this.updateIndexQueue.add({ type: 'update-variants-by-id', ctx: ctx.serialize(), ids });
     }
     }
 
 
     updateAsset(ctx: RequestContext, asset: Asset) {
     updateAsset(ctx: RequestContext, asset: Asset) {
-        this.addJobToQueue({ type: 'update-asset', ctx: ctx.serialize(), asset: asset as any });
+        this.updateIndexQueue.add({ type: 'update-asset', ctx: ctx.serialize(), asset: asset as any });
     }
     }
 
 
     deleteAsset(ctx: RequestContext, asset: Asset) {
     deleteAsset(ctx: RequestContext, asset: Asset) {
-        this.addJobToQueue({ type: 'delete-asset', ctx: ctx.serialize(), asset: asset as any });
-    }
-
-    private addJobToQueue(data: UpdateIndexQueueJobData) {
-        if (updateIndexQueue) {
-            return updateIndexQueue.add(data);
-        }
-    }
-
-    private sendMessage(job: Job<any>, message: WorkerMessage<any, any>) {
-        return new Promise((resolve, reject) => {
-            this.workerService.send(message).subscribe({
-                complete: () => resolve(true),
-                error: err => {
-                    Logger.error(err);
-                    reject(err);
-                },
-            });
-        });
+        this.updateIndexQueue.add({ type: 'delete-asset', ctx: ctx.serialize(), asset: asset as any });
     }
     }
 
 
-    private sendMessageWithProgress(job: Job<any>, message: WorkerMessage<any, ReindexMessageResponse>) {
+    private jobWithProgress(
+        job: Job<UpdateIndexQueueJobData>,
+        ob: Observable<ReindexMessageResponse>,
+    ): Promise<any> {
         return new Promise((resolve, reject) => {
         return new Promise((resolve, reject) => {
             let total: number | undefined;
             let total: number | undefined;
             let duration = 0;
             let duration = 0;
             let completed = 0;
             let completed = 0;
-            this.workerService.send(message).subscribe({
+            ob.subscribe({
                 next: (response: ReindexMessageResponse) => {
                 next: (response: ReindexMessageResponse) => {
                     if (!total) {
                     if (!total) {
                         total = response.total;
                         total = response.total;

+ 98 - 151
packages/elasticsearch-plugin/src/indexer.controller.ts

@@ -1,6 +1,5 @@
 import { Client } from '@elastic/elasticsearch';
 import { Client } from '@elastic/elasticsearch';
-import { Controller, Inject, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
-import { MessagePattern } from '@nestjs/microservices';
+import { Inject, Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
 import { unique } from '@vendure/common/lib/unique';
 import { unique } from '@vendure/common/lib/unique';
 import {
 import {
     Asset,
     Asset,
@@ -30,22 +29,17 @@ import { ELASTIC_SEARCH_OPTIONS, loggerCtx, PRODUCT_INDEX_NAME, VARIANT_INDEX_NA
 import { createIndices, deleteByChannel, deleteIndices } from './indexing-utils';
 import { createIndices, deleteByChannel, deleteIndices } from './indexing-utils';
 import { ElasticsearchOptions } from './options';
 import { ElasticsearchOptions } from './options';
 import {
 import {
-    AssignProductToChannelMessage,
-    AssignVariantToChannelMessage,
     BulkOperation,
     BulkOperation,
     BulkOperationDoc,
     BulkOperationDoc,
     BulkResponseBody,
     BulkResponseBody,
-    DeleteAssetMessage,
-    DeleteProductMessage,
-    DeleteVariantMessage,
+    ProductChannelMessageData,
     ProductIndexItem,
     ProductIndexItem,
-    ReindexMessage,
-    RemoveProductFromChannelMessage,
-    RemoveVariantFromChannelMessage,
-    UpdateAssetMessage,
-    UpdateProductMessage,
-    UpdateVariantMessage,
-    UpdateVariantsByIdMessage,
+    ReindexMessageData,
+    UpdateAssetMessageData,
+    UpdateProductMessageData,
+    UpdateVariantMessageData,
+    UpdateVariantsByIdMessageData,
+    VariantChannelMessageData,
     VariantIndexItem,
     VariantIndexItem,
 } from './types';
 } from './types';
 
 
@@ -70,7 +64,7 @@ export interface ReindexMessageResponse {
     duration: number;
     duration: number;
 }
 }
 
 
-@Controller()
+@Injectable()
 export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDestroy {
 export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDestroy {
     private client: Client;
     private client: Client;
     private asyncQueue = new AsyncQueue('elasticsearch-indexer', 5);
     private asyncQueue = new AsyncQueue('elasticsearch-indexer', 5);
@@ -96,160 +90,120 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
     /**
     /**
      * Updates the search index only for the affected product.
      * Updates the search index only for the affected product.
      */
      */
-    @MessagePattern(UpdateProductMessage.pattern)
-    updateProduct({
-        ctx: rawContext,
-        productId,
-    }: UpdateProductMessage['data']): Observable<UpdateProductMessage['response']> {
+    async updateProduct({ ctx: rawContext, productId }: UpdateProductMessageData): Promise<boolean> {
         const ctx = RequestContext.deserialize(rawContext);
         const ctx = RequestContext.deserialize(rawContext);
-        return asyncObservable(async () => {
-            await this.updateProductInternal(ctx, productId);
-            return true;
-        });
+        await this.updateProductInternal(ctx, productId);
+        return true;
     }
     }
 
 
     /**
     /**
      * Updates the search index only for the affected product.
      * Updates the search index only for the affected product.
      */
      */
-    @MessagePattern(DeleteProductMessage.pattern)
-    deleteProduct({
-        ctx: rawContext,
-        productId,
-    }: DeleteProductMessage['data']): Observable<DeleteProductMessage['response']> {
+    async deleteProduct({ ctx: rawContext, productId }: UpdateProductMessageData): Promise<boolean> {
         const ctx = RequestContext.deserialize(rawContext);
         const ctx = RequestContext.deserialize(rawContext);
-        return asyncObservable(async () => {
-            const product = await this.connection.getRepository(Product).findOne(productId);
-            if (!product) {
-                return false;
-            }
-            await this.deleteProductInternal(product, ctx.channelId);
-            const variants = await this.productVariantService.getVariantsByProductId(ctx, productId);
-            await this.deleteVariantsInternal(variants, ctx.channelId);
-            return true;
-        });
+        const product = await this.connection.getRepository(Product).findOne(productId);
+        if (!product) {
+            return false;
+        }
+        await this.deleteProductInternal(product, ctx.channelId);
+        const variants = await this.productVariantService.getVariantsByProductId(ctx, productId);
+        await this.deleteVariantsInternal(variants, ctx.channelId);
+        return true;
     }
     }
 
 
     /**
     /**
      * Updates the search index only for the affected product.
      * Updates the search index only for the affected product.
      */
      */
-    @MessagePattern(AssignProductToChannelMessage.pattern)
-    assignProductsToChannel({
+    async assignProductToChannel({
         ctx: rawContext,
         ctx: rawContext,
         productId,
         productId,
         channelId,
         channelId,
-    }: AssignProductToChannelMessage['data']): Observable<AssignProductToChannelMessage['response']> {
+    }: ProductChannelMessageData): Promise<boolean> {
         const ctx = RequestContext.deserialize(rawContext);
         const ctx = RequestContext.deserialize(rawContext);
-        return asyncObservable(async () => {
-            await this.updateProductInternal(ctx, productId);
-            const variants = await this.productVariantService.getVariantsByProductId(ctx, productId);
-            await this.updateVariantsInternal(
-                ctx,
-                variants.map(v => v.id),
-                channelId,
-            );
-            return true;
-        });
+        await this.updateProductInternal(ctx, productId);
+        const variants = await this.productVariantService.getVariantsByProductId(ctx, productId);
+        await this.updateVariantsInternal(
+            ctx,
+            variants.map(v => v.id),
+            channelId,
+        );
+        return true;
     }
     }
 
 
     /**
     /**
      * Updates the search index only for the affected product.
      * Updates the search index only for the affected product.
      */
      */
-    @MessagePattern(RemoveProductFromChannelMessage.pattern)
-    removeProductFromChannel({
+    async removeProductFromChannel({
         ctx: rawContext,
         ctx: rawContext,
         productId,
         productId,
         channelId,
         channelId,
-    }: RemoveProductFromChannelMessage['data']): Observable<RemoveProductFromChannelMessage['response']> {
+    }: ProductChannelMessageData): Promise<boolean> {
         const ctx = RequestContext.deserialize(rawContext);
         const ctx = RequestContext.deserialize(rawContext);
-        return asyncObservable(async () => {
-            const product = await this.connection.getRepository(Product).findOne(productId);
-            if (!product) {
-                return false;
-            }
-            await this.deleteProductInternal(product, channelId);
-            const variants = await this.productVariantService.getVariantsByProductId(ctx, productId);
-            await this.deleteVariantsInternal(variants, channelId);
-            return true;
-        });
+        const product = await this.connection.getRepository(Product).findOne(productId);
+        if (!product) {
+            return false;
+        }
+        await this.deleteProductInternal(product, channelId);
+        const variants = await this.productVariantService.getVariantsByProductId(ctx, productId);
+        await this.deleteVariantsInternal(variants, channelId);
+        return true;
     }
     }
 
 
-    @MessagePattern(AssignVariantToChannelMessage.pattern)
-    assignVariantToChannel({
+    async assignVariantToChannel({
         ctx: rawContext,
         ctx: rawContext,
         productVariantId,
         productVariantId,
         channelId,
         channelId,
-    }: AssignVariantToChannelMessage['data']): Observable<AssignVariantToChannelMessage['response']> {
+    }: VariantChannelMessageData): Promise<boolean> {
         const ctx = RequestContext.deserialize(rawContext);
         const ctx = RequestContext.deserialize(rawContext);
-        return asyncObservable(async () => {
-            await this.updateVariantsInternal(ctx, [productVariantId], channelId);
-            return true;
-        });
+        await this.updateVariantsInternal(ctx, [productVariantId], channelId);
+        return true;
     }
     }
 
 
-    @MessagePattern(RemoveVariantFromChannelMessage.pattern)
-    removeVariantFromChannel({
+    async removeVariantFromChannel({
         ctx: rawContext,
         ctx: rawContext,
         productVariantId,
         productVariantId,
         channelId,
         channelId,
-    }: RemoveVariantFromChannelMessage['data']): Observable<RemoveVariantFromChannelMessage['response']> {
+    }: VariantChannelMessageData): Promise<boolean> {
         const ctx = RequestContext.deserialize(rawContext);
         const ctx = RequestContext.deserialize(rawContext);
-        return asyncObservable(async () => {
-            const productVariant = await this.connection.getEntityOrThrow(
-                ctx,
-                ProductVariant,
-                productVariantId,
-                { relations: ['product', 'product.channels'] },
-            );
-            await this.deleteVariantsInternal([productVariant], channelId);
-
-            if (!productVariant.product.channels.find(c => idsAreEqual(c.id, channelId))) {
-                await this.deleteProductInternal(productVariant.product, channelId);
-            }
-            return true;
+        const productVariant = await this.connection.getEntityOrThrow(ctx, ProductVariant, productVariantId, {
+            relations: ['product', 'product.channels'],
         });
         });
+        await this.deleteVariantsInternal([productVariant], channelId);
+
+        if (!productVariant.product.channels.find(c => idsAreEqual(c.id, channelId))) {
+            await this.deleteProductInternal(productVariant.product, channelId);
+        }
+        return true;
     }
     }
 
 
     /**
     /**
      * Updates the search index only for the affected entities.
      * Updates the search index only for the affected entities.
      */
      */
-    @MessagePattern(UpdateVariantMessage.pattern)
-    updateVariants({
-        ctx: rawContext,
-        variantIds,
-    }: UpdateVariantMessage['data']): Observable<UpdateVariantMessage['response']> {
+    async updateVariants({ ctx: rawContext, variantIds }: UpdateVariantMessageData): Promise<boolean> {
         const ctx = RequestContext.deserialize(rawContext);
         const ctx = RequestContext.deserialize(rawContext);
-        return asyncObservable(async () => {
-            return this.asyncQueue.push(async () => {
-                await this.updateVariantsInternal(ctx, variantIds, ctx.channelId);
-                return true;
-            });
+        return this.asyncQueue.push(async () => {
+            await this.updateVariantsInternal(ctx, variantIds, ctx.channelId);
+            return true;
         });
         });
     }
     }
 
 
-    @MessagePattern(DeleteVariantMessage.pattern)
-    private deleteVaiants({
-        ctx: rawContext,
-        variantIds,
-    }: DeleteVariantMessage['data']): Observable<DeleteVariantMessage['response']> {
+    async deleteVariants({ ctx: rawContext, variantIds }: UpdateVariantMessageData): Promise<boolean> {
         const ctx = RequestContext.deserialize(rawContext);
         const ctx = RequestContext.deserialize(rawContext);
-        return asyncObservable(async () => {
-            const variants = await this.connection
-                .getRepository(ProductVariant)
-                .findByIds(variantIds, { relations: ['product'] });
-            const productIds = unique(variants.map(v => v.product.id));
-            for (const productId of productIds) {
-                await this.updateProductInternal(ctx, productId);
-            }
-            await this.deleteVariantsInternal(variants, ctx.channelId);
-            return true;
-        });
+        const variants = await this.connection
+            .getRepository(ProductVariant)
+            .findByIds(variantIds, { relations: ['product'] });
+        const productIds = unique(variants.map(v => v.product.id));
+        for (const productId of productIds) {
+            await this.updateProductInternal(ctx, productId);
+        }
+        await this.deleteVariantsInternal(variants, ctx.channelId);
+        return true;
     }
     }
 
 
-    @MessagePattern(UpdateVariantsByIdMessage.pattern)
     updateVariantsById({
     updateVariantsById({
         ctx: rawContext,
         ctx: rawContext,
         ids,
         ids,
-    }: UpdateVariantsByIdMessage['data']): Observable<UpdateVariantsByIdMessage['response']> {
+    }: UpdateVariantsByIdMessageData): Observable<ReindexMessageResponse> {
         const ctx = RequestContext.deserialize(rawContext);
         const ctx = RequestContext.deserialize(rawContext);
         const { batchSize } = this.options;
         const { batchSize } = this.options;
 
 
@@ -326,11 +280,7 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
         });
         });
     }
     }
 
 
-    @MessagePattern(ReindexMessage.pattern)
-    reindex({
-        ctx: rawContext,
-        dropIndices,
-    }: ReindexMessage['data']): Observable<ReindexMessage['response']> {
+    reindex({ ctx: rawContext, dropIndices }: ReindexMessageData): Observable<ReindexMessageResponse> {
         const ctx = RequestContext.deserialize(rawContext);
         const ctx = RequestContext.deserialize(rawContext);
         const { batchSize } = this.options;
         const { batchSize } = this.options;
 
 
@@ -401,34 +351,28 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
         });
         });
     }
     }
 
 
-    @MessagePattern(UpdateAssetMessage.pattern)
-    updateAsset(data: UpdateAssetMessage['data']): Observable<UpdateAssetMessage['response']> {
-        return asyncObservable(async () => {
-            const result1 = await this.updateAssetFocalPointForIndex(PRODUCT_INDEX_NAME, data.asset);
-            const result2 = await this.updateAssetFocalPointForIndex(VARIANT_INDEX_NAME, data.asset);
-            await this.client.indices.refresh({
-                index: [
-                    this.options.indexPrefix + PRODUCT_INDEX_NAME,
-                    this.options.indexPrefix + VARIANT_INDEX_NAME,
-                ],
-            });
-            return result1 && result2;
+    async updateAsset(data: UpdateAssetMessageData): Promise<boolean> {
+        const result1 = await this.updateAssetFocalPointForIndex(PRODUCT_INDEX_NAME, data.asset);
+        const result2 = await this.updateAssetFocalPointForIndex(VARIANT_INDEX_NAME, data.asset);
+        await this.client.indices.refresh({
+            index: [
+                this.options.indexPrefix + PRODUCT_INDEX_NAME,
+                this.options.indexPrefix + VARIANT_INDEX_NAME,
+            ],
         });
         });
+        return result1 && result2;
     }
     }
 
 
-    @MessagePattern(DeleteAssetMessage.pattern)
-    deleteAsset(data: DeleteAssetMessage['data']): Observable<DeleteAssetMessage['response']> {
-        return asyncObservable(async () => {
-            const result1 = await this.deleteAssetForIndex(PRODUCT_INDEX_NAME, data.asset);
-            const result2 = await this.deleteAssetForIndex(VARIANT_INDEX_NAME, data.asset);
-            await this.client.indices.refresh({
-                index: [
-                    this.options.indexPrefix + PRODUCT_INDEX_NAME,
-                    this.options.indexPrefix + VARIANT_INDEX_NAME,
-                ],
-            });
-            return result1 && result2;
+    async deleteAsset(data: UpdateAssetMessageData): Promise<boolean> {
+        const result1 = await this.deleteAssetForIndex(PRODUCT_INDEX_NAME, data.asset);
+        const result2 = await this.deleteAssetForIndex(VARIANT_INDEX_NAME, data.asset);
+        await this.client.indices.refresh({
+            index: [
+                this.options.indexPrefix + PRODUCT_INDEX_NAME,
+                this.options.indexPrefix + VARIANT_INDEX_NAME,
+            ],
         });
         });
+        return result1 && result2;
     }
     }
 
 
     private async updateAssetFocalPointForIndex(indexName: string, asset: Asset): Promise<boolean> {
     private async updateAssetFocalPointForIndex(indexName: string, asset: Asset): Promise<boolean> {
@@ -559,7 +503,7 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
                         isAuthorized: true,
                         isAuthorized: true,
                         session: {} as any,
                         session: {} as any,
                     });
                     });
-                    this.productVariantService.applyChannelPriceAndTax(variant, ctx);
+                    await this.productVariantService.applyChannelPriceAndTax(variant, ctx);
                     for (const languageCode of languageVariants) {
                     for (const languageCode of languageVariants) {
                         operations.push(
                         operations.push(
                             { update: { _id: this.getId(variant.id, channel.id, languageCode) } },
                             { update: { _id: this.getId(variant.id, channel.id, languageCode) } },
@@ -612,7 +556,7 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
                         v.channels.map(c => c.id).includes(channel.id),
                         v.channels.map(c => c.id).includes(channel.id),
                     );
                     );
                     for (const variant of variantsInChannel) {
                     for (const variant of variantsInChannel) {
-                        this.productVariantService.applyChannelPriceAndTax(variant, channelCtx);
+                        await this.productVariantService.applyChannelPriceAndTax(variant, channelCtx);
                     }
                     }
 
 
                     for (const languageCode of languageVariants) {
                     for (const languageCode of languageVariants) {
@@ -767,10 +711,13 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
     /**
     /**
      * Given an array of ProductVariants, this method applies the correct taxes and translations.
      * Given an array of ProductVariants, this method applies the correct taxes and translations.
      */
      */
-    private hydrateVariants(ctx: RequestContext, variants: ProductVariant[]): ProductVariant[] {
-        return variants
-            .map(v => this.productVariantService.applyChannelPriceAndTax(v, ctx))
-            .map(v => translateDeep(v, ctx.languageCode, ['product', 'collections']));
+    private async hydrateVariants(
+        ctx: RequestContext,
+        variants: ProductVariant[],
+    ): Promise<ProductVariant[]> {
+        return (
+            await Promise.all(variants.map(v => this.productVariantService.applyChannelPriceAndTax(v, ctx)))
+        ).map(v => translateDeep(v, ctx.languageCode, ['product', 'collections']));
     }
     }
 
 
     private createVariantIndexItem(
     private createVariantIndexItem(

+ 1 - 2
packages/elasticsearch-plugin/src/plugin.ts

@@ -196,6 +196,7 @@ import { ElasticsearchOptions, ElasticsearchRuntimeOptions, mergeWithDefaults }
         ElasticsearchIndexService,
         ElasticsearchIndexService,
         ElasticsearchService,
         ElasticsearchService,
         ElasticsearchHealthIndicator,
         ElasticsearchHealthIndicator,
+        ElasticsearchIndexerController,
         { provide: ELASTIC_SEARCH_OPTIONS, useFactory: () => ElasticsearchPlugin.options },
         { provide: ELASTIC_SEARCH_OPTIONS, useFactory: () => ElasticsearchPlugin.options },
     ],
     ],
     adminApiExtensions: { resolvers: [AdminElasticSearchResolver] },
     adminApiExtensions: { resolvers: [AdminElasticSearchResolver] },
@@ -213,7 +214,6 @@ import { ElasticsearchOptions, ElasticsearchRuntimeOptions, mergeWithDefaults }
         // which looks like possibly a TS/definitions bug.
         // which looks like possibly a TS/definitions bug.
         schema: () => generateSchemaExtensions(ElasticsearchPlugin.options as any),
         schema: () => generateSchemaExtensions(ElasticsearchPlugin.options as any),
     },
     },
-    workers: [ElasticsearchIndexerController],
 })
 })
 export class ElasticsearchPlugin implements OnApplicationBootstrap {
 export class ElasticsearchPlugin implements OnApplicationBootstrap {
     private static options: ElasticsearchRuntimeOptions;
     private static options: ElasticsearchRuntimeOptions;
@@ -252,7 +252,6 @@ export class ElasticsearchPlugin implements OnApplicationBootstrap {
         Logger.info(`Successfully connected to Elasticsearch instance at "${nodeName}"`, loggerCtx);
         Logger.info(`Successfully connected to Elasticsearch instance at "${nodeName}"`, loggerCtx);
 
 
         await this.elasticsearchService.createIndicesIfNotExists();
         await this.elasticsearchService.createIndicesIfNotExists();
-        this.elasticsearchIndexService.initJobQueue();
         this.healthCheckRegistryService.registerIndicatorFunction(() =>
         this.healthCheckRegistryService.registerIndicatorFunction(() =>
             this.elasticsearchHealthIndicator.isHealthy(),
             this.elasticsearchHealthIndicator.isHealthy(),
         );
         );

+ 1 - 41
packages/elasticsearch-plugin/src/types.ts

@@ -8,7 +8,7 @@ import {
     SearchResult,
     SearchResult,
 } from '@vendure/common/lib/generated-types';
 } from '@vendure/common/lib/generated-types';
 import { ID, JsonCompatible } from '@vendure/common/lib/shared-types';
 import { ID, JsonCompatible } from '@vendure/common/lib/shared-types';
-import { Asset, SerializedRequestContext, WorkerMessage } from '@vendure/core';
+import { Asset, SerializedRequestContext } from '@vendure/core';
 
 
 export type ElasticSearchInput = SearchInput & {
 export type ElasticSearchInput = SearchInput & {
     priceRange?: PriceRange;
     priceRange?: PriceRange;
@@ -190,46 +190,6 @@ export interface UpdateAssetMessageData {
     asset: JsonCompatible<Required<Asset>>;
     asset: JsonCompatible<Required<Asset>>;
 }
 }
 
 
-export class ReindexMessage extends WorkerMessage<ReindexMessageData, ReindexMessageResponse> {
-    static readonly pattern = 'Reindex';
-}
-export class UpdateVariantMessage extends WorkerMessage<UpdateVariantMessageData, boolean> {
-    static readonly pattern = 'UpdateProduct';
-}
-export class UpdateProductMessage extends WorkerMessage<UpdateProductMessageData, boolean> {
-    static readonly pattern = 'UpdateVariant';
-}
-export class DeleteVariantMessage extends WorkerMessage<UpdateVariantMessageData, boolean> {
-    static readonly pattern = 'DeleteProduct';
-}
-export class DeleteProductMessage extends WorkerMessage<UpdateProductMessageData, boolean> {
-    static readonly pattern = 'DeleteVariant';
-}
-export class UpdateVariantsByIdMessage extends WorkerMessage<
-    UpdateVariantsByIdMessageData,
-    ReindexMessageResponse
-> {
-    static readonly pattern = 'UpdateVariantsById';
-}
-export class AssignProductToChannelMessage extends WorkerMessage<ProductChannelMessageData, boolean> {
-    static readonly pattern = 'AssignProductToChannel';
-}
-export class RemoveProductFromChannelMessage extends WorkerMessage<ProductChannelMessageData, boolean> {
-    static readonly pattern = 'RemoveProductFromChannel';
-}
-export class AssignVariantToChannelMessage extends WorkerMessage<VariantChannelMessageData, boolean> {
-    static readonly pattern = 'AssignVariantToChannel';
-}
-export class RemoveVariantFromChannelMessage extends WorkerMessage<VariantChannelMessageData, boolean> {
-    static readonly pattern = 'RemoveVariantFromChannel';
-}
-export class UpdateAssetMessage extends WorkerMessage<UpdateAssetMessageData, boolean> {
-    static readonly pattern = 'UpdateAsset';
-}
-export class DeleteAssetMessage extends WorkerMessage<UpdateAssetMessageData, boolean> {
-    static readonly pattern = 'DeleteAsset';
-}
-
 type Maybe<T> = T | undefined;
 type Maybe<T> = T | undefined;
 type CustomMappingDefinition<Args extends any[], T extends string, R> = {
 type CustomMappingDefinition<Args extends any[], T extends string, R> = {
     graphQlType: T;
     graphQlType: T;

+ 4 - 8
packages/email-plugin/src/dev-mailbox.ts

@@ -12,15 +12,14 @@ import { EmailPluginDevModeOptions, EventWithContext } from './types';
  * An email inbox application that serves the contents of the dev mode `outputPath` directory.
  * An email inbox application that serves the contents of the dev mode `outputPath` directory.
  */
  */
 export class DevMailbox {
 export class DevMailbox {
-    server: http.Server;
     private handleMockEventFn: (
     private handleMockEventFn: (
         handler: EmailEventHandler<string, any>,
         handler: EmailEventHandler<string, any>,
         event: EventWithContext,
         event: EventWithContext,
     ) => void | undefined;
     ) => void | undefined;
 
 
     serve(options: EmailPluginDevModeOptions) {
     serve(options: EmailPluginDevModeOptions) {
-        const { outputPath, handlers, mailboxPort } = options;
-        const server = express();
+        const { outputPath, handlers } = options;
+        const server = express.Router();
         server.get('/', (req, res) => {
         server.get('/', (req, res) => {
             res.sendFile(path.join(__dirname, '../../dev-mailbox.html'));
             res.sendFile(path.join(__dirname, '../../dev-mailbox.html'));
         });
         });
@@ -61,17 +60,14 @@ export class DevMailbox {
             const content = await this.getEmail(outputPath, fileName);
             const content = await this.getEmail(outputPath, fileName);
             res.send(content);
             res.send(content);
         });
         });
-        this.server = server.listen(mailboxPort);
+
+        return server;
     }
     }
 
 
     handleMockEvent(handler: (handler: EmailEventHandler<string, any>, event: EventWithContext) => void) {
     handleMockEvent(handler: (handler: EmailEventHandler<string, any>, event: EventWithContext) => void) {
         this.handleMockEventFn = handler;
         this.handleMockEventFn = handler;
     }
     }
 
 
-    destroy() {
-        this.server.close();
-    }
-
     private async getEmailList(outputPath: string) {
     private async getEmailList(outputPath: string) {
         const list = await fs.readdir(outputPath);
         const list = await fs.readdir(outputPath);
         const contents: any[] = [];
         const contents: any[] = [];

+ 0 - 29
packages/email-plugin/src/email-processor.controller.ts

@@ -1,29 +0,0 @@
-import { Controller, Inject, OnModuleInit } from '@nestjs/common';
-import { MessagePattern } from '@nestjs/microservices';
-import { asyncObservable } from '@vendure/core';
-import { Observable } from 'rxjs';
-
-import { EMAIL_PLUGIN_OPTIONS } from './constants';
-import { EmailProcessor } from './email-processor';
-import { EmailPluginOptions, EmailWorkerMessage } from './types';
-
-/**
- * Runs on the Worker process and does the actual work of generating and sending the emails.
- */
-@Controller()
-export class EmailProcessorController extends EmailProcessor implements OnModuleInit {
-    constructor(@Inject(EMAIL_PLUGIN_OPTIONS) protected options: EmailPluginOptions) {
-        super(options);
-    }
-
-    async onModuleInit() {
-        await super.init();
-    }
-
-    @MessagePattern(EmailWorkerMessage.pattern)
-    sendEmail(data: EmailWorkerMessage['data']): Observable<EmailWorkerMessage['response']> {
-        return asyncObservable(async () => {
-            return this.process(data);
-        });
-    }
-}

+ 4 - 2
packages/email-plugin/src/email-processor.ts

@@ -1,9 +1,10 @@
+import { Inject, Injectable } from '@nestjs/common';
 import { InternalServerError, Logger } from '@vendure/core';
 import { InternalServerError, Logger } from '@vendure/core';
 import fs from 'fs-extra';
 import fs from 'fs-extra';
 
 
 import { deserializeAttachments } from './attachment-utils';
 import { deserializeAttachments } from './attachment-utils';
 import { isDevModeOptions } from './common';
 import { isDevModeOptions } from './common';
-import { loggerCtx } from './constants';
+import { EMAIL_PLUGIN_OPTIONS, loggerCtx } from './constants';
 import { HandlebarsMjmlGenerator } from './handlebars-mjml-generator';
 import { HandlebarsMjmlGenerator } from './handlebars-mjml-generator';
 import { NodemailerEmailSender } from './nodemailer-email-sender';
 import { NodemailerEmailSender } from './nodemailer-email-sender';
 import { TemplateLoader } from './template-loader';
 import { TemplateLoader } from './template-loader';
@@ -20,13 +21,14 @@ import {
  * the EmailPlugin. It is arranged this way primarily to accommodate easier testing, so that the
  * the EmailPlugin. It is arranged this way primarily to accommodate easier testing, so that the
  * tests can be run without needing all the JobQueue stuff which would require a full e2e test.
  * tests can be run without needing all the JobQueue stuff which would require a full e2e test.
  */
  */
+@Injectable()
 export class EmailProcessor {
 export class EmailProcessor {
     protected templateLoader: TemplateLoader;
     protected templateLoader: TemplateLoader;
     protected emailSender: EmailSender;
     protected emailSender: EmailSender;
     protected generator: EmailGenerator;
     protected generator: EmailGenerator;
     protected transport: EmailTransportOptions;
     protected transport: EmailTransportOptions;
 
 
-    constructor(protected options: EmailPluginOptions) {}
+    constructor(@Inject(EMAIL_PLUGIN_OPTIONS) protected options: EmailPluginOptions) {}
 
 
     async init() {
     async init() {
         this.templateLoader = new TemplateLoader(this.options.templatePath);
         this.templateLoader = new TemplateLoader(this.options.templatePath);

+ 0 - 2
packages/email-plugin/src/plugin.spec.ts

@@ -9,7 +9,6 @@ import {
     Order,
     Order,
     OrderStateTransitionEvent,
     OrderStateTransitionEvent,
     PluginCommonModule,
     PluginCommonModule,
-    ProcessContextModule,
     RequestContext,
     RequestContext,
     VendureEvent,
     VendureEvent,
 } from '@vendure/core';
 } from '@vendure/core';
@@ -40,7 +39,6 @@ describe('EmailPlugin', () => {
                     type: 'sqljs',
                     type: 'sqljs',
                     retryAttempts: 0,
                     retryAttempts: 0,
                 }),
                 }),
-                ProcessContextModule.forRoot(),
                 PluginCommonModule,
                 PluginCommonModule,
                 EmailPlugin.init({
                 EmailPlugin.init({
                     templatePath: path.join(__dirname, '../test-templates'),
                     templatePath: path.join(__dirname, '../test-templates'),

+ 18 - 52
packages/email-plugin/src/plugin.ts

@@ -1,31 +1,24 @@
-import { OnApplicationBootstrap } from '@nestjs/common';
+import { MiddlewareConsumer, NestModule, OnApplicationBootstrap } from '@nestjs/common';
 import { ModuleRef } from '@nestjs/core';
 import { ModuleRef } from '@nestjs/core';
 import {
 import {
-    createProxyHandler,
     EventBus,
     EventBus,
     Injector,
     Injector,
     JobQueue,
     JobQueue,
     JobQueueService,
     JobQueueService,
     Logger,
     Logger,
-    OnVendureBootstrap,
-    OnVendureClose,
     PluginCommonModule,
     PluginCommonModule,
-    RuntimeVendureConfig,
     Type,
     Type,
     VendurePlugin,
     VendurePlugin,
-    WorkerService,
 } from '@vendure/core';
 } from '@vendure/core';
 
 
 import { isDevModeOptions } from './common';
 import { isDevModeOptions } from './common';
-import { EMAIL_PLUGIN_OPTIONS } from './constants';
+import { EMAIL_PLUGIN_OPTIONS, loggerCtx } from './constants';
 import { DevMailbox } from './dev-mailbox';
 import { DevMailbox } from './dev-mailbox';
 import { EmailProcessor } from './email-processor';
 import { EmailProcessor } from './email-processor';
-import { EmailProcessorController } from './email-processor.controller';
 import { EmailEventHandler, EmailEventHandlerWithAsyncData } from './event-handler';
 import { EmailEventHandler, EmailEventHandlerWithAsyncData } from './event-handler';
 import {
 import {
     EmailPluginDevModeOptions,
     EmailPluginDevModeOptions,
     EmailPluginOptions,
     EmailPluginOptions,
-    EmailWorkerMessage,
     EventWithContext,
     EventWithContext,
     IntermediateEmailDetails,
     IntermediateEmailDetails,
 } from './types';
 } from './types';
@@ -127,13 +120,12 @@ import {
  *   handlers: defaultEmailHandlers,
  *   handlers: defaultEmailHandlers,
  *   templatePath: path.join(__dirname, 'vendure/email/templates'),
  *   templatePath: path.join(__dirname, 'vendure/email/templates'),
  *   outputPath: path.join(__dirname, 'test-emails'),
  *   outputPath: path.join(__dirname, 'test-emails'),
- *   mailboxPort: 5003,
  * })
  * })
  * ```
  * ```
  *
  *
  * ### Dev mailbox
  * ### Dev mailbox
  *
  *
- * In dev mode, specifying the optional `mailboxPort` will start a webmail-like interface available at the `/mailbox` path, e.g.
+ * In dev mode, a webmail-like interface available at the `/mailbox` path, e.g.
  * http://localhost:3000/mailbox. This is a simple way to view the output of all emails generated by the EmailPlugin while in dev mode.
  * http://localhost:3000/mailbox. This is a simple way to view the output of all emails generated by the EmailPlugin while in dev mode.
  *
  *
  * ## Troubleshooting SMTP Connections
  * ## Troubleshooting SMTP Connections
@@ -169,11 +161,9 @@ import {
  */
  */
 @VendurePlugin({
 @VendurePlugin({
     imports: [PluginCommonModule],
     imports: [PluginCommonModule],
-    providers: [{ provide: EMAIL_PLUGIN_OPTIONS, useFactory: () => EmailPlugin.options }],
-    workers: [EmailProcessorController],
-    configuration: config => EmailPlugin.configure(config),
+    providers: [{ provide: EMAIL_PLUGIN_OPTIONS, useFactory: () => EmailPlugin.options }, EmailProcessor],
 })
 })
-export class EmailPlugin implements OnApplicationBootstrap, OnVendureBootstrap, OnVendureClose {
+export class EmailPlugin implements OnApplicationBootstrap, NestModule {
     private static options: EmailPluginOptions | EmailPluginDevModeOptions;
     private static options: EmailPluginOptions | EmailPluginDevModeOptions;
     private devMailbox: DevMailbox | undefined;
     private devMailbox: DevMailbox | undefined;
     private jobQueue: JobQueue<IntermediateEmailDetails> | undefined;
     private jobQueue: JobQueue<IntermediateEmailDetails> | undefined;
@@ -183,7 +173,7 @@ export class EmailPlugin implements OnApplicationBootstrap, OnVendureBootstrap,
     constructor(
     constructor(
         private eventBus: EventBus,
         private eventBus: EventBus,
         private moduleRef: ModuleRef,
         private moduleRef: ModuleRef,
-        private workerService: WorkerService,
+        private emailProcessor: EmailProcessor,
         private jobQueueService: JobQueueService,
         private jobQueueService: JobQueueService,
     ) {}
     ) {}
 
 
@@ -195,29 +185,6 @@ export class EmailPlugin implements OnApplicationBootstrap, OnVendureBootstrap,
         return EmailPlugin;
         return EmailPlugin;
     }
     }
 
 
-    /** @internal */
-    static configure(config: RuntimeVendureConfig): RuntimeVendureConfig {
-        if (isDevModeOptions(this.options) && this.options.mailboxPort !== undefined) {
-            const route = 'mailbox';
-            config.apiOptions.middleware.push({
-                handler: createProxyHandler({ port: this.options.mailboxPort, route, label: 'Dev Mailbox' }),
-                route,
-            });
-        }
-        return config;
-    }
-
-    /** @internal */
-    async onVendureBootstrap() {
-        const options = EmailPlugin.options;
-
-        if (isDevModeOptions(options) && options.mailboxPort !== undefined) {
-            this.devMailbox = new DevMailbox();
-            this.devMailbox.serve(options);
-            this.devMailbox.handleMockEvent((handler, event) => this.handleEvent(handler, event));
-        }
-    }
-
     /** @internal */
     /** @internal */
     async onApplicationBootstrap(): Promise<void> {
     async onApplicationBootstrap(): Promise<void> {
         const options = EmailPlugin.options;
         const options = EmailPlugin.options;
@@ -229,24 +196,23 @@ export class EmailPlugin implements OnApplicationBootstrap, OnVendureBootstrap,
             this.testingProcessor = new EmailProcessor(options);
             this.testingProcessor = new EmailProcessor(options);
             await this.testingProcessor.init();
             await this.testingProcessor.init();
         } else {
         } else {
-            this.jobQueue = this.jobQueueService.createQueue({
+            this.jobQueue = await this.jobQueueService.createQueue({
                 name: 'send-email',
                 name: 'send-email',
                 process: job => {
                 process: job => {
-                    return new Promise((resolve, reject) => {
-                        this.workerService.send(new EmailWorkerMessage(job.data)).subscribe({
-                            complete: () => resolve(),
-                            error: err => reject(err),
-                        });
-                    });
+                    return this.emailProcessor.process(job.data);
                 },
                 },
             });
             });
         }
         }
     }
     }
 
 
-    /** @internal */
-    async onVendureClose() {
-        if (this.devMailbox) {
-            this.devMailbox.destroy();
+    configure(consumer: MiddlewareConsumer) {
+        const options = EmailPlugin.options;
+
+        if (isDevModeOptions(options)) {
+            Logger.info('Creating dev mailbox middleware', loggerCtx);
+            this.devMailbox = new DevMailbox();
+            consumer.apply(this.devMailbox.serve(options)).forRoutes(options.route);
+            this.devMailbox.handleMockEvent((handler, event) => this.handleEvent(handler, event));
         }
         }
     }
     }
 
 
@@ -262,7 +228,7 @@ export class EmailPlugin implements OnApplicationBootstrap, OnVendureBootstrap,
         handler: EmailEventHandler | EmailEventHandlerWithAsyncData<any>,
         handler: EmailEventHandler | EmailEventHandlerWithAsyncData<any>,
         event: EventWithContext,
         event: EventWithContext,
     ) {
     ) {
-        Logger.debug(`Handling event "${handler.type}"`, 'EmailPlugin');
+        Logger.debug(`Handling event "${handler.type}"`, loggerCtx);
         const { type } = handler;
         const { type } = handler;
         try {
         try {
             const injector = new Injector(this.moduleRef);
             const injector = new Injector(this.moduleRef);
@@ -280,7 +246,7 @@ export class EmailPlugin implements OnApplicationBootstrap, OnVendureBootstrap,
                 await this.testingProcessor.process(result);
                 await this.testingProcessor.process(result);
             }
             }
         } catch (e) {
         } catch (e) {
-            Logger.error(e.message, 'EmailPlugin', e.stack);
+            Logger.error(e.message, loggerCtx, e.stack);
         }
         }
     }
     }
 }
 }

+ 3 - 10
packages/email-plugin/src/types.ts

@@ -1,7 +1,6 @@
 import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { Omit } from '@vendure/common/lib/omit';
 import { Omit } from '@vendure/common/lib/omit';
-import { JsonCompatible } from '@vendure/common/lib/shared-types';
-import { Injector, RequestContext, VendureEvent, WorkerMessage } from '@vendure/core';
+import { Injector, RequestContext, VendureEvent } from '@vendure/core';
 import { Attachment } from 'nodemailer/lib/mailer';
 import { Attachment } from 'nodemailer/lib/mailer';
 
 
 import { EmailEventHandler } from './event-handler';
 import { EmailEventHandler } from './event-handler';
@@ -93,11 +92,9 @@ export interface EmailPluginDevModeOptions extends Omit<EmailPluginOptions, 'tra
     outputPath: string;
     outputPath: string;
     /**
     /**
      * @description
      * @description
-     * If set, a "mailbox" server will be started which will serve the contents of the
-     * `outputPath` similar to a web-based email client, available at the route `/mailbox`,
-     * e.g. http://localhost:3000/mailbox.
+     * The route to the dev mailbox server.
      */
      */
-    mailboxPort?: number;
+    route: string;
 }
 }
 
 
 /**
 /**
@@ -391,10 +388,6 @@ export type IntermediateEmailDetails = {
     attachments: SerializedAttachment[];
     attachments: SerializedAttachment[];
 };
 };
 
 
-export class EmailWorkerMessage extends WorkerMessage<IntermediateEmailDetails, boolean> {
-    static readonly pattern = 'send-email';
-}
-
 /**
 /**
  * @description
  * @description
  * Configures the {@link EmailEventHandler} to handle a particular channel & languageCode
  * Configures the {@link EmailEventHandler} to handle a particular channel & languageCode

+ 4 - 1
packages/pub-sub-plugin/src/pub-sub-job-queue-strategy.ts

@@ -57,7 +57,10 @@ export class PubSubJobQueueStrategy extends InjectableJobQueueStrategy implement
         });
         });
     }
     }
 
 
-    start<Data extends JobData<Data> = {}>(queueName: string, process: (job: Job<Data>) => Promise<any>) {
+    async start<Data extends JobData<Data> = {}>(
+        queueName: string,
+        process: (job: Job<Data>) => Promise<any>,
+    ) {
         if (!this.hasInitialized) {
         if (!this.hasInitialized) {
             this.started.set(queueName, process);
             this.started.set(queueName, process);
             return;
             return;

+ 0 - 7
packages/testing/src/config/test-config.ts

@@ -60,11 +60,4 @@ export const testConfig: Required<VendureConfig> = mergeConfig(defaultConfig, {
         assetStorageStrategy: new TestingAssetStorageStrategy(),
         assetStorageStrategy: new TestingAssetStorageStrategy(),
         assetPreviewStrategy: new TestingAssetPreviewStrategy(),
         assetPreviewStrategy: new TestingAssetPreviewStrategy(),
     },
     },
-    workerOptions: {
-        runInMainProcess: true,
-        transport: Transport.TCP,
-        options: {
-            port: 3051,
-        },
-    },
 });
 });

+ 7 - 7
packages/testing/src/data-population/populate-for-testing.ts

@@ -1,5 +1,5 @@
 /* tslint:disable:no-console */
 /* tslint:disable:no-console */
-import { INestApplication, INestMicroservice } from '@nestjs/common';
+import { INestApplicationContext } from '@nestjs/common';
 import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { VendureConfig } from '@vendure/core';
 import { VendureConfig } from '@vendure/core';
 import { importProductsFromCsv, populateCollections, populateInitialData } from '@vendure/core/cli';
 import { importProductsFromCsv, populateCollections, populateInitialData } from '@vendure/core/cli';
@@ -12,17 +12,17 @@ import { populateCustomers } from './populate-customers';
 /**
 /**
  * Clears all tables from the database and populates with (deterministic) random data.
  * Clears all tables from the database and populates with (deterministic) random data.
  */
  */
-export async function populateForTesting(
+export async function populateForTesting<T extends INestApplicationContext>(
     config: Required<VendureConfig>,
     config: Required<VendureConfig>,
-    bootstrapFn: (config: VendureConfig) => Promise<[INestApplication, INestMicroservice | undefined]>,
+    bootstrapFn: (config: VendureConfig) => Promise<T>,
     options: TestServerOptions,
     options: TestServerOptions,
-): Promise<[INestApplication, INestMicroservice | undefined]> {
+): Promise<T> {
     (config.dbConnectionOptions as any).logging = false;
     (config.dbConnectionOptions as any).logging = false;
     const logging = options.logging === undefined ? true : options.logging;
     const logging = options.logging === undefined ? true : options.logging;
     const originalRequireVerification = config.authOptions.requireVerification;
     const originalRequireVerification = config.authOptions.requireVerification;
     config.authOptions.requireVerification = false;
     config.authOptions.requireVerification = false;
 
 
-    const [app, worker] = await bootstrapFn(config);
+    const app = await bootstrapFn(config);
 
 
     const logFn = (message: string) => (logging ? console.log(message) : null);
     const logFn = (message: string) => (logging ? console.log(message) : null);
 
 
@@ -32,10 +32,10 @@ export async function populateForTesting(
     await populateCustomers(options.customerCount ?? 10, config, logging);
     await populateCustomers(options.customerCount ?? 10, config, logging);
 
 
     config.authOptions.requireVerification = originalRequireVerification;
     config.authOptions.requireVerification = originalRequireVerification;
-    return [app, worker];
+    return app;
 }
 }
 
 
-async function populateProducts(app: INestApplication, productsCsvPath: string, logging: boolean) {
+async function populateProducts(app: INestApplicationContext, productsCsvPath: string, logging: boolean) {
     const importResult = await importProductsFromCsv(app, productsCsvPath, LanguageCode.en);
     const importResult = await importProductsFromCsv(app, productsCsvPath, LanguageCode.en);
     if (importResult.errors && importResult.errors.length) {
     if (importResult.errors && importResult.errors.length) {
         console.log(`${importResult.errors.length} errors encountered when importing product data:`);
         console.log(`${importResult.errors.length} errors encountered when importing product data:`);

+ 8 - 41
packages/testing/src/test-server.ts

@@ -1,11 +1,7 @@
-import { INestApplication, INestMicroservice } from '@nestjs/common';
+import { INestApplication } from '@nestjs/common';
 import { NestFactory } from '@nestjs/core';
 import { NestFactory } from '@nestjs/core';
-import { DefaultLogger, Logger, VendureConfig } from '@vendure/core';
-import {
-    preBootstrapConfig,
-    runBeforeBootstrapHooks,
-    runBeforeWorkerBootstrapHooks,
-} from '@vendure/core/dist/bootstrap';
+import { DefaultLogger, JobQueueService, Logger, VendureConfig } from '@vendure/core';
+import { preBootstrapConfig } from '@vendure/core/dist/bootstrap';
 
 
 import { populateForTesting } from './data-population/populate-for-testing';
 import { populateForTesting } from './data-population/populate-for-testing';
 import { getInitializerFor } from './initializers/initializers';
 import { getInitializerFor } from './initializers/initializers';
@@ -20,7 +16,6 @@ import { TestServerOptions } from './types';
  */
  */
 export class TestServer {
 export class TestServer {
     public app: INestApplication;
     public app: INestApplication;
-    public worker?: INestMicroservice;
 
 
     constructor(private vendureConfig: Required<VendureConfig>) {}
     constructor(private vendureConfig: Required<VendureConfig>) {}
 
 
@@ -55,16 +50,7 @@ export class TestServer {
      * start and stop a Vendure instance multiple times without re-populating data.
      * start and stop a Vendure instance multiple times without re-populating data.
      */
      */
     async bootstrap() {
     async bootstrap() {
-        const [app, worker] = await this.bootstrapForTesting(this.vendureConfig);
-        if (app) {
-            this.app = app;
-        } else {
-            console.error(`Could not bootstrap app`);
-            process.exit(1);
-        }
-        if (worker) {
-            this.worker = worker;
-        }
+        this.app = await this.bootstrapForTesting(this.vendureConfig);
     }
     }
 
 
     /**
     /**
@@ -75,9 +61,6 @@ export class TestServer {
     async destroy() {
     async destroy() {
         // allow a grace period of any outstanding async tasks to complete
         // allow a grace period of any outstanding async tasks to complete
         await new Promise(resolve => global.setTimeout(resolve, 500));
         await new Promise(resolve => global.setTimeout(resolve, 500));
-        if (this.worker) {
-            await this.worker.close();
-        }
         await this.app.close();
         await this.app.close();
     }
     }
 
 
@@ -111,22 +94,17 @@ export class TestServer {
         testingConfig: Required<VendureConfig>,
         testingConfig: Required<VendureConfig>,
         options: TestServerOptions,
         options: TestServerOptions,
     ): Promise<void> {
     ): Promise<void> {
-        const [app, worker] = await populateForTesting(testingConfig, this.bootstrapForTesting, {
+        const app = await populateForTesting(testingConfig, this.bootstrapForTesting, {
             logging: false,
             logging: false,
             ...options,
             ...options,
         });
         });
-        if (worker) {
-            await worker.close();
-        }
         await app.close();
         await app.close();
     }
     }
 
 
     /**
     /**
      * Bootstraps an instance of the Vendure server for testing against.
      * Bootstraps an instance of the Vendure server for testing against.
      */
      */
-    private async bootstrapForTesting(
-        userConfig: Partial<VendureConfig>,
-    ): Promise<[INestApplication, INestMicroservice | undefined]> {
+    private async bootstrapForTesting(userConfig: Partial<VendureConfig>): Promise<INestApplication> {
         const config = await preBootstrapConfig(userConfig);
         const config = await preBootstrapConfig(userConfig);
         Logger.useLogger(config.logger);
         Logger.useLogger(config.logger);
         const appModule = await import('@vendure/core/dist/app.module');
         const appModule = await import('@vendure/core/dist/app.module');
@@ -136,21 +114,10 @@ export class TestServer {
                 cors: config.apiOptions.cors,
                 cors: config.apiOptions.cors,
                 logger: new Logger(),
                 logger: new Logger(),
             });
             });
-            let worker: INestMicroservice | undefined;
-            await runBeforeBootstrapHooks(config, app);
             await app.listen(config.apiOptions.port);
             await app.listen(config.apiOptions.port);
-            if (config.workerOptions.runInMainProcess) {
-                const workerModule = await import('@vendure/core/dist/worker/worker.module');
-                worker = await NestFactory.createMicroservice(workerModule.WorkerModule, {
-                    transport: config.workerOptions.transport,
-                    logger: new Logger(),
-                    options: config.workerOptions.options,
-                });
-                await runBeforeWorkerBootstrapHooks(config, worker);
-                await worker.listenAsync();
-            }
+            await app.get(JobQueueService).start();
             DefaultLogger.restoreOriginalLogLevel();
             DefaultLogger.restoreOriginalLogLevel();
-            return [app, worker];
+            return app;
         } catch (e) {
         } catch (e) {
             console.log(e);
             console.log(e);
             throw e;
             throw e;