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 inb69c2f7311
) 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 inb69c2f7311
is no longer needed with this commit.
This commit is contained in:
parent
8c17f5b72c
commit
2461ed303c
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
width = target_dimensions.width;
|
||||
height = target_dimensions.height;
|
||||
|
@ -102,12 +103,12 @@ async function optimize(imageData, fileName, width, height, settings) {
|
|||
mozJpegDefaultOptions
|
||||
);
|
||||
|
||||
const finalSize = result.byteLength
|
||||
const finalSize = result.byteLength;
|
||||
logIfDebug(`Worker post reencode file: ${finalSize}`);
|
||||
logIfDebug(`Reduction: ${(initialSize / finalSize).toFixed(1)}x speedup`);
|
||||
|
||||
if (finalSize < 20000) {
|
||||
throw "Final size suspciously small, discarding optimizations"
|
||||
throw "Final size suspciously small, discarding optimizations";
|
||||
}
|
||||
|
||||
let transferrable = Uint8Array.from(result).buffer; // decoded was allocated inside WASM so it **cannot** be transfered to another context, need to copy by value
|
||||
|
@ -132,7 +133,7 @@ onmessage = async function (e) {
|
|||
type: "file",
|
||||
file: optimized,
|
||||
fileName: e.data.fileName,
|
||||
fileId: e.data.fileId
|
||||
fileId: e.data.fileId,
|
||||
},
|
||||
[optimized]
|
||||
);
|
||||
|
@ -142,31 +143,27 @@ onmessage = async function (e) {
|
|||
type: "error",
|
||||
file: e.data.file,
|
||||
fileName: e.data.fileName,
|
||||
fileId: e.data.fileId
|
||||
fileId: e.data.fileId,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "install":
|
||||
await loadLibs(e.data.settings);
|
||||
postMessage({ type: "installed" });
|
||||
break;
|
||||
default:
|
||||
logIfDebug(`Sorry, we are out of ${e}.`);
|
||||
}
|
||||
};
|
||||
|
||||
async function loadLibs(settings){
|
||||
|
||||
async function loadLibs(settings) {
|
||||
if (self.codecs) return;
|
||||
|
||||
if (!self.loadedMozJpeg) {
|
||||
importScripts(settings.mozjpeg_script);
|
||||
self.loadedMozJpeg = true;
|
||||
}
|
||||
|
||||
if (!self.loadedResizeScript) {
|
||||
importScripts(settings.resize_script);
|
||||
self.loadedResizeScript = true;
|
||||
}
|
||||
|
||||
let encoderModuleOverrides = {
|
||||
locateFile: function(path, prefix) {
|
||||
locateFile: function (path, prefix) {
|
||||
// if it's a mem init file, use a custom dir
|
||||
if (path.endsWith(".wasm")) return settings.mozjpeg_wasm;
|
||||
// otherwise, use the default, the prefix (JS file's dir) + the path
|
||||
|
@ -181,5 +178,5 @@ async function loadLibs(settings){
|
|||
const { resize } = wasm_bindgen;
|
||||
await wasm_bindgen(settings.resize_wasm);
|
||||
|
||||
self.codecs = {mozjpeg_enc: mozjpeg_enc_module, resize: resize};
|
||||
self.codecs = { mozjpeg_enc: mozjpeg_enc_module, resize: resize };
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue