DEV: Move loadLibs to explicit install message in media-optimization-worker (#14707)

DEV: Move loadLibs to explicit install message in media-optimization-worker (#14707)

Previously, loadLibs was called inside the optimize function of the media-optimization-worker, which meant that it could be hit multiple times causing load errors (as seen in b69c2f7311836b8e35719a4d24cb60020fd7234d)

This commit moves that call to a specific message handler (the install message) for the service worker, and refactors the service for the media-optimization-worker to wait for this installation to complete before continuing with processing image optimizations.

This way, we know for sure based on promises and worker messages that the worker is installed and has all required libraries loaded before we continue on with attempting any processing. The change made in b69c2f7311836b8e35719a4d24cb60020fd7234d is no longer needed with this commit.

diff --git a/app/assets/javascripts/discourse/app/services/media-optimization-worker.js b/app/assets/javascripts/discourse/app/services/media-optimization-worker.js
index 37f2387..4feec51 100644
--- a/app/assets/javascripts/discourse/app/services/media-optimization-worker.js
+++ b/app/assets/javascripts/discourse/app/services/media-optimization-worker.js
@@ -4,6 +4,22 @@ import { Promise } from "rsvp";
 import { fileToImageData } from "discourse/lib/media-optimization-utils";
 import { getAbsoluteURL, getURLWithCDN } from "discourse-common/lib/get-url";
 
+/**
+ * This worker follows a particular promise/callback flow to ensure
+ * that the media-optimization-worker is installed and has its libraries
+ * loaded before optimizations can happen. The flow:
+ *
+ * 1. optimizeImage called
+ * 2. worker initialized and started
+ * 3. message handlers for worker registered
+ * 4. "install" message posted to worker
+ * 5. "installed" message received from worker
+ * 6. optimizeImage continues, posting "compress" message to worker
+ *
+ * When the worker is being installed, all other calls to optimizeImage
+ * will wait for the "installed" message to be handled before continuing
+ * with any image optimization work.
+ */
 export default class MediaOptimizationWorkerService extends Service {
   appEvents = getOwner(this).lookup("service:app-events");
   worker = null;
@@ -11,35 +27,7 @@ export default class MediaOptimizationWorkerService extends Service {
   currentComposerUploadData = null;
   promiseResolvers = null;
 
-  startWorker() {
-    this.logIfDebug("Starting media-optimization-worker");
-    this.worker = new Worker(this.workerUrl); // TODO come up with a workaround for FF that lacks type: module support
-  }
-
-  stopWorker() {
-    if (this.worker) {
-      this.logIfDebug("Stopping media-optimization-worker...");
-      this.worker.terminate();
-      this.worker = null;
-    }
-  }
-
-  ensureAvailiableWorker() {
-    if (!this.worker) {
-      this.startWorker();
-      this.registerMessageHandler();
-      this.appEvents.on("composer:closed", this, "stopWorker");
-    }
-  }
-
-  logIfDebug(message) {
-    if (this.siteSettings.composer_media_optimization_debug_mode) {
-      // eslint-disable-next-line no-console
-      console.log(message);
-    }
-  }
-
-  optimizeImage(data, opts = {}) {
+  async optimizeImage(data, opts = {}) {
     this.usingUppy = data.id && data.id.includes("uppy");
     this.promiseResolvers = this.promiseResolvers || {};
     this.stopWorkerOnError = opts.hasOwnProperty("stopWorkerOnError")
@@ -57,7 +45,8 @@ export default class MediaOptimizationWorkerService extends Service {
     ) {
       return this.usingUppy ? Promise.resolve() : data;
     }
-    this.ensureAvailiableWorker();
+    await this.ensureAvailiableWorker();
+
     return new Promise(async (resolve) => {
       this.logIfDebug(`Transforming ${file.name}`);
 
@@ -85,18 +74,6 @@ export default class MediaOptimizationWorkerService extends Service {
           width: imageData.width,
           height: imageData.height,
           settings: {
-            mozjpeg_script: getURLWithCDN(
-              "/javascripts/squoosh/mozjpeg_enc.js"
-            ),
-            mozjpeg_wasm: getURLWithCDN(
-              "/javascripts/squoosh/mozjpeg_enc.wasm"
-            ),
-            resize_script: getURLWithCDN(
-              "/javascripts/squoosh/squoosh_resize.js"
-            ),
-            resize_wasm: getURLWithCDN(
-              "/javascripts/squoosh/squoosh_resize_bg.wasm"
-            ),
             resize_threshold: this.siteSettings
               .composer_media_optimization_image_resize_dimensions_threshold,
             resize_target: this.siteSettings
@@ -116,6 +93,54 @@ export default class MediaOptimizationWorkerService extends Service {
     });
   }
 
+  async ensureAvailiableWorker() {
+    if (this.worker && this.workerInstalled) {
+      return Promise.resolve();
+    }
+    if (this.installPromise) {
+      return this.installPromise;
+    }
+    return this.install();
+  }
+
+  async install() {
+    this.installPromise = new Promise((resolve) => {
+      this.afterInstalled = resolve;
+      this.logIfDebug("Installing worker.");
+      this.startWorker();
+      this.registerMessageHandler();
+      this.worker.postMessage({
+        type: "install",
+        settings: {
+          mozjpeg_script: getURLWithCDN("/javascripts/squoosh/mozjpeg_enc.js"),
+          mozjpeg_wasm: getURLWithCDN("/javascripts/squoosh/mozjpeg_enc.wasm"),
+          resize_script: getURLWithCDN(
+            "/javascripts/squoosh/squoosh_resize.js"
+          ),
+          resize_wasm: getURLWithCDN(
+            "/javascripts/squoosh/squoosh_resize_bg.wasm"
+          ),
+        },
+      });
+      this.appEvents.on("composer:closed", this, "stopWorker");
+    });
+    return this.installPromise;
+  }
+
+  startWorker() {
+    this.logIfDebug("Starting media-optimization-worker");
+    this.worker = new Worker(this.workerUrl); // TODO come up with a workaround for FF that lacks type: module support
+  }
+
+  stopWorker() {
+    if (this.worker) {
+      this.logIfDebug("Stopping media-optimization-worker...");
+      this.workerInstalled = false;
+      this.worker.terminate();
+      this.worker = null;
+    }
+  }
+
   registerMessageHandler() {
     this.worker.onmessage = (e) => {
       switch (e.data.type) {
@@ -153,9 +178,23 @@ export default class MediaOptimizationWorkerService extends Service {
             );
           }
           break;
+        case "installed":
+          this.logIfDebug("Worker installed.");
+          this.workerInstalled = true;
+          this.afterInstalled();
+          this.afterInstalled = null;
+          this.installPromise = null;
+          break;
         default:
           this.logIfDebug(`Sorry, we are out of ${e}.`);
       }
     };
   }
+
+  logIfDebug(message) {
+    if (this.siteSettings.composer_media_optimization_debug_mode) {
+      // eslint-disable-next-line no-console
+      console.log(message);
+    }
+  }
 }
diff --git a/public/javascripts/media-optimization-worker.js b/public/javascripts/media-optimization-worker.js
index e57e362..b3fd8f0 100644
--- a/public/javascripts/media-optimization-worker.js
+++ b/public/javascripts/media-optimization-worker.js
@@ -2,10 +2,10 @@ function resizeWithAspect(
   input_width,
   input_height,
   target_width,
-  target_height,
+  target_height
 ) {
   if (!target_width && !target_height) {
-    throw Error('Need to specify at least width or height when resizing');
+    throw Error("Need to specify at least width or height when resizing");
   }
 
   if (target_width && target_height) {
@@ -33,9 +33,6 @@ function logIfDebug(message) {
 }
 
 async function optimize(imageData, fileName, width, height, settings) {
-
-  await loadLibs(settings);
-
   const mozJpegDefaultOptions = {
     quality: settings.encode_quality,
     baseline: false,
@@ -63,7 +60,11 @@ async function optimize(imageData, fileName, width, height, settings) {
   // resize
   if (width > settings.resize_threshold) {
     try {
-      const target_dimensions = resizeWithAspect(width, height, settings.resize_target);
+      const target_dimensions = resizeWithAspect(
+        width,
+        height,
+        settings.resize_target
+      );
       const resizeResult = self.codecs.resize(
         new Uint8ClampedArray(imageData),
         width, //in
@@ -75,12 +76,12 @@ async function optimize(imageData, fileName, width, height, settings) {
         settings.resize_linear_rgb
       );
       if (resizeResult[3] !== 255) {
-        throw "Image corrupted during resize. Falling back to the original for encode"
+        throw "Image corrupted during resize. Falling back to the original for encode";
       }
       maybeResized = new ImageData(
         resizeResult,
         target_dimensions.width,
-        target_dimensions.height,
+        target_dimensions.height
       ).data;

[... diff too long, it was truncated ...]

GitHub sha: 2461ed303c21b8782cc66e548dbcbb7c2c65c9c1

This commit appears in #14707 which was approved by Falco. It was merged by martin.