From 12ffec8f571f343ebc3a2a5e94ea2989d3182f0b Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Sat, 12 Sep 2026 07:14:40 +0200 Subject: [PATCH 1/5] refactor: experimental unbundle of worker DO NOT MERGE This is incomplete but just looking at some of the changes required to provide worker as a separate file - only for kmw so far. Build-bot: skip Test-bot: skip --- web/src/app/browser/build.sh | 12 +- web/src/app/browser/src/debug-main.ts | 12 -- .../browser/src/{release-main.ts => main.ts} | 0 web/src/app/webview/build.sh | 4 +- web/src/app/webview/map-polyfill-bundler.js | 2 +- web/src/app/webview/src/debug-main.ts | 12 -- .../webview/src/{release-main.ts => main.ts} | 0 web/src/engine/predictive-text/build.sh | 2 +- .../worker-main/src/node/index.ts | 1 - .../worker-main/src/node/mappedWorker.ts | 29 +-- .../worker-main/src/node/node-worker.ts | 13 +- .../src/node/sourcemappedWorker.ts | 19 -- .../predictive-text/worker-main/src/unwrap.ts | 11 - .../worker-main/src/web/index.ts | 1 - .../worker-main/src/web/sourcemappedWorker.ts | 37 ---- .../worker-main/src/web/web-worker.ts | 33 +-- .../worker-dummy-integration.tests.js | 2 +- .../headless/worker-trie-integration.tests.js | 2 +- .../worker-thread/build-polyfiller.js | 196 ------------------ .../worker-thread/build-wrapper.js | 121 ----------- .../predictive-text/worker-thread/build.sh | 13 +- .../worker-thread/package.json | 14 +- .../tests/test-runner/cases/worker.tests.ts | 15 +- .../engine/main/inputProcessor.tests.ts | 2 +- .../engine/main/languageProcessor.tests.ts | 2 +- 25 files changed, 53 insertions(+), 502 deletions(-) delete mode 100644 web/src/app/browser/src/debug-main.ts rename web/src/app/browser/src/{release-main.ts => main.ts} (100%) delete mode 100644 web/src/app/webview/src/debug-main.ts rename web/src/app/webview/src/{release-main.ts => main.ts} (100%) delete mode 100644 web/src/engine/predictive-text/worker-main/src/node/sourcemappedWorker.ts delete mode 100644 web/src/engine/predictive-text/worker-main/src/unwrap.ts delete mode 100644 web/src/engine/predictive-text/worker-main/src/web/sourcemappedWorker.ts delete mode 100644 web/src/engine/predictive-text/worker-thread/build-polyfiller.js delete mode 100644 web/src/engine/predictive-text/worker-thread/build-wrapper.js diff --git a/web/src/app/browser/build.sh b/web/src/app/browser/build.sh index 41fc9c5230b..3a3dfbf6150 100755 --- a/web/src/app/browser/build.sh +++ b/web/src/app/browser/build.sh @@ -49,13 +49,13 @@ compile_and_copy() { BUILD_ROOT="${KEYMAN_ROOT}/web/build/app/browser" SRC_ROOT="${KEYMAN_ROOT}/web/src/app/browser/src" - node_es_bundle "${SRC_ROOT}/debug-main.js" \ + node_es_bundle "${SRC_ROOT}/main.js" \ --out "${BUILD_ROOT}/debug/keymanweb.js" \ --charset "utf8" \ --sourceRoot "@keymanapp/keyman/web/build/app/browser/debug" \ --target "es6" - node_es_bundle "${SRC_ROOT}/release-main.js" \ + node_es_bundle "${SRC_ROOT}/main.js" \ --out "${BUILD_ROOT}/release/keymanweb.js" \ --charset "utf8" \ --profile "${BUILD_ROOT}/filesize-profile.log" \ @@ -72,6 +72,14 @@ compile_and_copy() { mkdir -p "$KEYMAN_ROOT/web/build/app/resources/osk" cp -R "$KEYMAN_ROOT/web/src/resources/osk/." "$KEYMAN_ROOT/web/build/app/resources/osk/" + cp "${KEYMAN_ROOT}/web/src/engine/predictive-text/worker-thread/build/lib/worker-main.js" "${BUILD_ROOT}/debug/worker-main.js" + cp "${KEYMAN_ROOT}/web/src/engine/predictive-text/worker-thread/build/lib/worker-main.js.map" "${BUILD_ROOT}/debug/worker-main.js.map" + cp "${KEYMAN_ROOT}/web/src/engine/predictive-text/worker-thread/build/lib/worker-main.d.ts" "${BUILD_ROOT}/debug/worker-main.d.ts" + + # TODO: rename to lm-worker.js? do we keep debug/release distinction through .js vs .min.js or via folder? + cp "${KEYMAN_ROOT}/web/src/engine/predictive-text/worker-thread/build/lib/worker-main.min.js" "${BUILD_ROOT}/release/worker-main.js" + cp "${KEYMAN_ROOT}/web/src/engine/predictive-text/worker-thread/build/lib/worker-main.min.js.map" "${BUILD_ROOT}/release/worker-main.js.map" + # Update the build/publish copy of our build artifacts prepare diff --git a/web/src/app/browser/src/debug-main.ts b/web/src/app/browser/src/debug-main.ts deleted file mode 100644 index aa6b23a2a4b..00000000000 --- a/web/src/app/browser/src/debug-main.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { KeymanEngine } from './keymanEngine.js' -import { SourcemappedWorker } from '@keymanapp/lexical-model-layer/web' - -/** -* Determine path and protocol of executing script, setting them as -* construction defaults. -*/ -const ss = (document.currentScript as HTMLScriptElement)?.src; -const sPath = ss ? ss.substring(0, ss.lastIndexOf('/') + 1) : './'; - -// @ts-ignore -window['keyman'] = new KeymanEngine(SourcemappedWorker, sPath); \ No newline at end of file diff --git a/web/src/app/browser/src/release-main.ts b/web/src/app/browser/src/main.ts similarity index 100% rename from web/src/app/browser/src/release-main.ts rename to web/src/app/browser/src/main.ts diff --git a/web/src/app/webview/build.sh b/web/src/app/webview/build.sh index cce218ba6d6..8e150d0796c 100755 --- a/web/src/app/webview/build.sh +++ b/web/src/app/webview/build.sh @@ -44,13 +44,13 @@ compile_and_copy() { BUILD_ROOT="${KEYMAN_ROOT}/web/build/app/webview" SRC_ROOT="${KEYMAN_ROOT}/web/src/app/webview/src" - node_es_bundle "${SRC_ROOT}/debug-main.js" \ + node_es_bundle "${SRC_ROOT}/main.js" \ --out "${BUILD_ROOT}/debug/keymanweb-webview.js" \ --charset "utf8" \ --sourceRoot "@keymanapp/keyman/web/build/app/webview/debug" \ --target "es6" - node_es_bundle "${SRC_ROOT}/release-main.js" \ + node_es_bundle "${SRC_ROOT}/main.js" \ --out "${BUILD_ROOT}/release/keymanweb-webview.js" \ --charset "utf8" \ --profile "${BUILD_ROOT}/filesize-profile.log" \ diff --git a/web/src/app/webview/map-polyfill-bundler.js b/web/src/app/webview/map-polyfill-bundler.js index 257f897e70f..e4d25867392 100644 --- a/web/src/app/webview/map-polyfill-bundler.js +++ b/web/src/app/webview/map-polyfill-bundler.js @@ -20,7 +20,7 @@ const commonConfig = { sourceRoot: '@keymanapp/keyman/web/build/app/webview/debug/' }; -await prepareTslibTreeshaking(commonConfig, /worker-main\.wrapped(?:\.min)?\.js/); +await prepareTslibTreeshaking(commonConfig, /worker-main(?:\.min)?\.js/); await esbuild.build(commonConfig); diff --git a/web/src/app/webview/src/debug-main.ts b/web/src/app/webview/src/debug-main.ts deleted file mode 100644 index aa6b23a2a4b..00000000000 --- a/web/src/app/webview/src/debug-main.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { KeymanEngine } from './keymanEngine.js' -import { SourcemappedWorker } from '@keymanapp/lexical-model-layer/web' - -/** -* Determine path and protocol of executing script, setting them as -* construction defaults. -*/ -const ss = (document.currentScript as HTMLScriptElement)?.src; -const sPath = ss ? ss.substring(0, ss.lastIndexOf('/') + 1) : './'; - -// @ts-ignore -window['keyman'] = new KeymanEngine(SourcemappedWorker, sPath); \ No newline at end of file diff --git a/web/src/app/webview/src/release-main.ts b/web/src/app/webview/src/main.ts similarity index 100% rename from web/src/app/webview/src/release-main.ts rename to web/src/app/webview/src/main.ts diff --git a/web/src/engine/predictive-text/build.sh b/web/src/engine/predictive-text/build.sh index ac6d2a670b0..c73cc2da93a 100755 --- a/web/src/engine/predictive-text/build.sh +++ b/web/src/engine/predictive-text/build.sh @@ -32,7 +32,7 @@ builder_describe_outputs \ build:templates "/web/src/engine/predictive-text/templates/build/obj/index.js" \ build:wordbreakers "/web/src/engine/predictive-text/wordbreakers/build/main/obj/index.js" \ build:worker-main "/web/src/engine/predictive-text/worker-main/build/lib/index.mjs" \ - build:worker-thread "/web/src/engine/predictive-text/worker-thread/build/lib/worker-main.wrapped.js" + build:worker-thread "/web/src/engine/predictive-text/worker-thread/build/lib/worker-main.js" #### Build action definitions #### diff --git a/web/src/engine/predictive-text/worker-main/src/node/index.ts b/web/src/engine/predictive-text/worker-main/src/node/index.ts index dc5957207b4..d9156a9ef5b 100644 --- a/web/src/engine/predictive-text/worker-main/src/node/index.ts +++ b/web/src/engine/predictive-text/worker-main/src/node/index.ts @@ -1,6 +1,5 @@ export { LMLayer } from '../lmlayer.js'; export { NodeWorker } from './node-worker.js'; -export { SourcemappedWorker } from './sourcemappedWorker.js'; export interface WorkerFactory { constructInstance(): Worker diff --git a/web/src/engine/predictive-text/worker-main/src/node/mappedWorker.ts b/web/src/engine/predictive-text/worker-main/src/node/mappedWorker.ts index 8b3d9d2b7b9..a2acbd68da4 100644 --- a/web/src/engine/predictive-text/worker-main/src/node/mappedWorker.ts +++ b/web/src/engine/predictive-text/worker-main/src/node/mappedWorker.ts @@ -9,6 +9,8 @@ import { URL } from 'node:url'; /** * Defines mappings from Node Worker signatures to WebWorker signatures + * + * TODO: move this to a separate module, no need for it to be embedded string */ const nodeWorkerToWebWorkerMappingSource = ` import { parentPort } from 'node:worker_threads'; @@ -42,32 +44,23 @@ function importScripts(...args) { const self = globalThis; self.postMessage = postMessage; self.importScripts = importScripts; +self.self = self; // make it global! +// Start off by importing the main worker itself +// importScripts('${import.meta.dirname}/../../../worker-thread/build/lib/worker-main.js'); +console.dir(import.meta); +importScripts('${import.meta.dirname}/worker-main.js'); `; + + /** * Uses the Node version of Workers to provide proper, authentic separate-thread * 'sandboxing'. Also intercepts and interprets certain WebWorker method signatures * necessary to run the WebWorker-oriented worker code. - * - * Alternatively, only after writing this did I discover this package: - * https://github.com/developit/web-worker. They also ran one notable issue I did: - * Node 18.x, at least, does not support use of Node Blobs for construction of a - * Worker: https://github.com/developit/web-worker/pull/32... unlike Web Workers. - * - * So... Base64-encoded Data URLs it is. - * - * What we have here is perfectly fine for now, but if we need more complicated - * cross-platform Worker support in the future, it may be wise to swap to use of - * that package. */ export class MappedWorker extends worker.Worker implements Worker { - constructor(scriptStr: string) { - const concatenatedScript = ` - ${nodeWorkerToWebWorkerMappingSource} - - ${scriptStr} - `; - const buffer = Buffer.from(concatenatedScript); + constructor() { + const buffer = Buffer.from(nodeWorkerToWebWorkerMappingSource); const dataSrc = "data:text/javascript;base64," + buffer.toString('base64'); //@ts-ignore super(new URL(dataSrc)); diff --git a/web/src/engine/predictive-text/worker-main/src/node/node-worker.ts b/web/src/engine/predictive-text/worker-main/src/node/node-worker.ts index e773ea9ffa4..8d18badf0de 100644 --- a/web/src/engine/predictive-text/worker-main/src/node/node-worker.ts +++ b/web/src/engine/predictive-text/worker-main/src/node/node-worker.ts @@ -2,19 +2,10 @@ * Keyman is copyright (C) SIL Global. MIT License. */ import { MappedWorker } from "./mappedWorker.js"; -import { unwrap } from '../unwrap.js'; - -import { LMLayerWorkerCode, LMLayerWorkerSourcemapComment } from "@keymanapp/lm-worker/worker-main.wrapped.min.js"; +// TODO: eliminate MappedWorker as its own thing export class NodeWorker { static constructInstance(): Worker { - let scriptStr = unwrap(LMLayerWorkerCode); - const srcMapUrlIndex = scriptStr.indexOf("//# sourceMappingURL") - if(srcMapUrlIndex > 0) { - scriptStr = scriptStr.substring(0, srcMapUrlIndex); - } - - scriptStr += '\n' + LMLayerWorkerSourcemapComment; - return new MappedWorker(scriptStr); + return new MappedWorker(); } } \ No newline at end of file diff --git a/web/src/engine/predictive-text/worker-main/src/node/sourcemappedWorker.ts b/web/src/engine/predictive-text/worker-main/src/node/sourcemappedWorker.ts deleted file mode 100644 index a8141939ce9..00000000000 --- a/web/src/engine/predictive-text/worker-main/src/node/sourcemappedWorker.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { MappedWorker } from "./mappedWorker.js"; -import { unwrap } from '../unwrap.js'; - -import { LMLayerWorkerCode, LMLayerWorkerSourcemapComment } from "@keymanapp/lm-worker/worker-main.wrapped.js"; - -export class SourcemappedWorker { - static constructInstance(): Worker { - let scriptStr = unwrap(LMLayerWorkerCode); - - // If this is definitively set to either true or false, tree-shaking can take effect. - // An imported const variable doesn't seem to do it, though. - // if(false) { - scriptStr += '\n' + LMLayerWorkerSourcemapComment; - // } - let worker = new MappedWorker(scriptStr); - - return worker as any as Worker; - } -} \ No newline at end of file diff --git a/web/src/engine/predictive-text/worker-main/src/unwrap.ts b/web/src/engine/predictive-text/worker-main/src/unwrap.ts deleted file mode 100644 index 6cb84f4da3a..00000000000 --- a/web/src/engine/predictive-text/worker-main/src/unwrap.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Given a function, this utility returns the source code within it, as a string. - * This is intended to unwrap the "wrapped" source code created in the LMLayerWorker - * build process. - * - * @param fn The function whose body will be returned. - */ -export function unwrap(encodedSrc: string): string { - // There used to be more to this, but now it's a pretty simple passthrough! - return encodedSrc; -} \ No newline at end of file diff --git a/web/src/engine/predictive-text/worker-main/src/web/index.ts b/web/src/engine/predictive-text/worker-main/src/web/index.ts index 299f51a0346..25bdb2522ff 100644 --- a/web/src/engine/predictive-text/worker-main/src/web/index.ts +++ b/web/src/engine/predictive-text/worker-main/src/web/index.ts @@ -1,6 +1,5 @@ export { LMLayer } from '../lmlayer.js'; export { WebWorker } from './web-worker.js'; -export { SourcemappedWorker } from './sourcemappedWorker.js'; export interface WorkerFactory { constructInstance(): Worker diff --git a/web/src/engine/predictive-text/worker-main/src/web/sourcemappedWorker.ts b/web/src/engine/predictive-text/worker-main/src/web/sourcemappedWorker.ts deleted file mode 100644 index 81a7c1d4a35..00000000000 --- a/web/src/engine/predictive-text/worker-main/src/web/sourcemappedWorker.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { unwrap } from '../unwrap.js'; -import { LMLayerWorkerCode, LMLayerWorkerSourcemapComment } from "@keymanapp/lm-worker/worker-main.wrapped.js"; - -export class SourcemappedWorker { - // the only difference to WebWorker is that this class uses - // the unminified LM* blobs - - static constructInstance(): Worker { - return new Worker(this.asBlobURI(LMLayerWorkerCode)); - } - - /** - * Converts the INSIDE of a function into a blob URI that can - * be passed as a valid URI for a Worker. - * @param fn Function whose body will be referenced by a URI. - * - * This function makes the following possible: - * - * let worker = new Worker(LMLayer.asBlobURI(function myWorkerCode () { - * postMessage('inside Web Worker') - * function onmessage(event) { - * // handle message inside Web Worker. - * } - * })); - */ - static asBlobURI(encodedSrc: string): string { - let code = unwrap(encodedSrc); - - // If this is definitively set to either true or false, tree-shaking can take effect. - // An imported const variable doesn't seem to do it, though. - // if(false) { - code += '\n' + LMLayerWorkerSourcemapComment; - // } - let blob = new Blob([code], { type: 'text/javascript' }); - return URL.createObjectURL(blob); - } -} \ No newline at end of file diff --git a/web/src/engine/predictive-text/worker-main/src/web/web-worker.ts b/web/src/engine/predictive-text/worker-main/src/web/web-worker.ts index 8e2a8edbbd3..c55a5cd630f 100644 --- a/web/src/engine/predictive-text/worker-main/src/web/web-worker.ts +++ b/web/src/engine/predictive-text/worker-main/src/web/web-worker.ts @@ -1,37 +1,16 @@ /* * Keyman is copyright (C) SIL Global. MIT License. */ -import { unwrap } from '../unwrap.js'; -import { LMLayerWorkerCode, LMLayerWorkerSourcemapComment } from "@keymanapp/lm-worker/worker-main.wrapped.min.js"; export class WebWorker { static constructInstance(): Worker { - return new Worker(this.asBlobURI(LMLayerWorkerCode)); + return new Worker(this.workerURI()); } - /** - * Converts the INSIDE of a function into a blob URI that can - * be passed as a valid URI for a Worker. - * @param fn Function whose body will be referenced by a URI. - * - * This function makes the following possible: - * - * let worker = new Worker(LMLayer.asBlobURI(function myWorkerCode () { - * postMessage('inside Web Worker') - * function onmessage(event) { - * // handle message inside Web Worker. - * } - * })); - */ - static asBlobURI(encodedSrc: string): string { - let code = unwrap(encodedSrc); - - // If this is definitively set to either true or false, tree-shaking can take effect. - // An imported const variable doesn't seem to do it, though. - // if(false) { - code += '\n' + LMLayerWorkerSourcemapComment; - // } - const blob = new Blob([code], { type: 'text/javascript' }); - return URL.createObjectURL(blob); + static workerURI(): string { + // TODO: worker-thread generates worker-main.js,... whaaa + // TODO: worker-main.min.js? + // TODO: paths + return './worker-main.js'; } } \ No newline at end of file diff --git a/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-dummy-integration.tests.js b/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-dummy-integration.tests.js index d546edb7040..7fcce07e5db 100644 --- a/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-dummy-integration.tests.js +++ b/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-dummy-integration.tests.js @@ -4,7 +4,7 @@ import fs from 'fs'; import { createRequire } from 'module'; const require = createRequire(import.meta.url); -import { LMLayer, SourcemappedWorker as Worker } from '#./node/index.js'; +import { LMLayer, NodeWorker as Worker } from '#./node/index.js'; import { capabilities, iGotDistractedByHazel } from '@keymanapp/common-test-resources/model-helpers.mjs'; /* diff --git a/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-trie-integration.tests.js b/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-trie-integration.tests.js index 70583a978ee..aa248a3c443 100644 --- a/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-trie-integration.tests.js +++ b/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-trie-integration.tests.js @@ -3,7 +3,7 @@ import { assert } from 'chai'; import { createRequire } from 'module'; const require = createRequire(import.meta.url); -import { LMLayer, SourcemappedWorker as Worker } from '#./node/index.js'; +import { LMLayer, NodeWorker as Worker } from '#./node/index.js'; import { capabilities } from '@keymanapp/common-test-resources/model-helpers.mjs'; /* diff --git a/web/src/engine/predictive-text/worker-thread/build-polyfiller.js b/web/src/engine/predictive-text/worker-thread/build-polyfiller.js deleted file mode 100644 index eab268e9867..00000000000 --- a/web/src/engine/predictive-text/worker-thread/build-polyfiller.js +++ /dev/null @@ -1,196 +0,0 @@ -import fs from 'fs'; - -import SourceMapCombiner from 'combine-source-map'; -import convertSourceMap from 'convert-source-map'; // Transforms sourcemaps among various common formats. - // Base64, stringified-JSON, end-of-file comment... - -import esbuild from 'esbuild'; - -let sourceFromArgs; -let destFromArgs; - -function doHelp(errMessage) { - if(errMessage) { - console.error(errMessage + '\n'); - } - console.log(` -Summary: - Creates a polyfilled version of the lm-worker to ensure compatibility with the needs of - Keyman for Android when run on Android 5.0 / API 21 without an updated Chrome webview. - -Usage: - node build-polyfiller.js [options...] - -Parameters: - : Fully-bundled and compiled JS file to be polyfilled. - -Options: - --help Shows this script's documentation - --out Specifies the destination path for the polyfilled output. - - If missing, the output will be placed next to the source and given - the same path, but with '.polyfilled.js' replacing the original '.js' - extension. - - Either way, a minified version will also be output, using the same - path but with the final '.js' replaced by '.min.js'. -` ); - process.exit(errMessage ? 1 : 0); -} - -if(process.argv.length > 2) { - for(let i = 2; i < process.argv.length; i++) { - const arg = process.argv[i]; - - switch(arg) { - case '--help': - doHelp(); - break; - case '--out': - destFromArgs = process.argv[++i]; - break; - default: - if(!sourceFromArgs) { - sourceFromArgs = arg; - } else { - doHelp("Input file can only be specified once; aborting"); - } - } - } -} else { - // Display help + abort. - doHelp("Required parameters missing"); -} - -if(!sourceFromArgs || sourceFromArgs.substring(sourceFromArgs.length - 3) != '.js') { - doHelp("No input file has been specified; aborting."); -} - -const sourceFile = sourceFromArgs; -const destFile = destFromArgs || sourceFromArgs.substring(0, sourceFromArgs.length - 3) + '.polyfilled.js'; -const minDestFile = destFile.substring(0, destFile.length - 3) + '.min.js'; - -let loadPolyfill = function(scriptFile, sourceMapFile) { - // May want to retool the pathing somewhat! - return { - source: fs.readFileSync(scriptFile).toString(), - sourceFile: sourceMapFile || scriptFile - }; -} - -let loadCompiledModuleFilePair = function(file, mapFilename) { - let module = fs.readFileSync(file); - let moduleSourcemapJSON = JSON.parse(fs.readFileSync(file + '.map').toString()); - - // May want to retool the pathing somewhat! - return { - plainSource: `${module}`, - sourceMapAsJSON: moduleSourcemapJSON, - sourceFile: mapFilename || file, - - /**The source + inlined sourcemap-as-comment used by `combine-source-map`. */ - get source() { - let jsonAsBuffer = Buffer.from(JSON.stringify(this.sourceMapAsJSON)); - return `${this.plainSource}\n${convertSourceMap.fromJSON(jsonAsBuffer).toComment()}`; - } - }; -} - -function concatScriptsAndSourcemaps(files, finalName, separatorFile) { - let combiner = SourceMapCombiner.create(finalName); - - let finalConcatenationArray = []; - let lineCountThusFar = 0; - - for(let filePairing of files) { - let offset = { - line: lineCountThusFar - }; - - console.log(`- ${filePairing.sourceFile}`); - combiner = combiner.addFile(filePairing, offset); - - let rawSourceToConcat = filePairing.plainSource || filePairing.source; - lineCountThusFar += rawSourceToConcat.split('\n').length + 1; // Not sure why it needs the fudge-factor, but it does. - - finalConcatenationArray.push(filePairing); - if(filePairing != files[files.length-1]) { - combiner = combiner.addFile(separatorFile); - finalConcatenationArray.push(separatorFile); - } - } - - let bundledSource = finalConcatenationArray.map((pair => pair.plainSource || pair.source)).join(''); - - return { - script: bundledSource, - sourcemapJSON: JSON.parse(convertSourceMap.fromBase64(combiner.base64()).toJSON()), - scriptFilename: finalName - } -} - -// Centralized? - -console.log("Pass 1: worker + polyfill concatenation"); - -let separatorFile = { - source: ` - - `, - sourceFile: '' -} - -let sourceFileSet = [ - // Needed for Android / Chromium browser pre-41. - loadPolyfill('../../../node_modules/string.prototype.codepointat/codepointat.js', 'src/polyfills/string.codepointat.js'), - // Needed for Android / Chromium browser pre-45. - // Not used in this codebase, but used by some compiled model defaults. - loadPolyfill('src/polyfills/array.from.js', 'src/polyfills/array.from.js'), - // Needed for Android / Chromium browser pre-45. - loadPolyfill('src/polyfills/array.fill.js', 'src/polyfills/array.fill.js'), - // Needed for Android / Chromium browser pre-45. - loadPolyfill('src/polyfills/array.findIndex.js', 'src/polyfills/array.findIndex.js'), - // Needed for Android / Chromium browser pre-47. - loadPolyfill('src/polyfills/array.includes.js', 'src/polyfills/array.includes.js'), - // For Object.values, for iteration over object-based associate arrays. - // Needed for Android / Chromium browser pre-54. - loadPolyfill('src/polyfills/object.values.js', 'src/polyfills/object.values.js'), - // Needed to support Symbol.iterator, as used by the correction algorithm. - // Needed for Android / Chromium browser pre-43. - loadPolyfill('src/polyfills/symbol-es6.min.js', 'src/polyfills/symbol-es6.min.js'), - loadCompiledModuleFilePair(sourceFile, sourceFile) -]; - -let fullWorkerConcatenation = concatScriptsAndSourcemaps(sourceFileSet, destFile, separatorFile); - -// New stage: cleaning the sourcemaps - -// Sources are being passed into the sourcemap concatenator via our working directory. -let sourceRoot = '@keymanapp/keyman/web/src/engine/predictive-text/worker-thread/'; -fullWorkerConcatenation.sourcemapJSON.sourceRoot = sourceRoot; - -// End "cleaning the sourcemaps" - -console.log(); -console.log("Pass 2: Output intermediate state and perform minification"); - -// IMPORTANT: Remove file-end sourcemap ref comment and replace it! -fullWorkerConcatenation.script = fullWorkerConcatenation.script.substring(0, fullWorkerConcatenation.script.lastIndexOf('//# sourceMappingURL')); -fullWorkerConcatenation.script += `//# sourceMappingURL=${fullWorkerConcatenation.scriptFilename}.map`; - -fs.writeFileSync(fullWorkerConcatenation.scriptFilename, fullWorkerConcatenation.script); -if(fullWorkerConcatenation.sourcemapJSON) { - fs.writeFileSync(`${fullWorkerConcatenation.scriptFilename}.map`, convertSourceMap.fromObject(fullWorkerConcatenation.sourcemapJSON).toJSON()); -} - -await esbuild.build({ - entryPoints: [destFile], - sourcemap: 'external', - sourcesContent: true, - minify: true, - // Do NOT enable - will break under Android 5.0 / Chrome 35 environments, likely through Chrome 42. - // https://caniuse.com/mdn-javascript_builtins_function_name_configurable_true - keepNames: false, - target: 'es6', - outfile: minDestFile -}); diff --git a/web/src/engine/predictive-text/worker-thread/build-wrapper.js b/web/src/engine/predictive-text/worker-thread/build-wrapper.js deleted file mode 100644 index 66fff07b6f3..00000000000 --- a/web/src/engine/predictive-text/worker-thread/build-wrapper.js +++ /dev/null @@ -1,121 +0,0 @@ -import fs from 'fs'; - -import convertSourcemap from 'convert-source-map'; // Transforms sourcemaps among various common formats. - // Base64, stringified-JSON, end-of-file comment... - -let INCLUDE_SRCMAPS = false; - -let sourceFromArgs; -let destFromArgs; - -function doHelp(errMessage) { - if(errMessage) { - console.error(errMessage + '\n'); - } - - console.log(` -Summary: - Creates a "wrapped" version of the lm-worker for compilation into and inclusion as part - of a different JS bundle for later use as the core of a predictive-text WebWorker. - -Usage: - node build-wrapper.js [options...] - -Parameters: - : Fully-bundled and compiled JS file to be wrapped. - -Options: - --help Shows this script's documentation - --out Specifies the destination path for the wrapped output. - - If missing, the output will be placed next to the source and given - the same path, but with '.wrapped.js' replacing the original '.js' - extension. - --sourceMap Includes the script's original sourcemaps within the wrapped output -` ); - process.exit(errMessage ? 1 : 0); -} - -if(process.argv.length > 2) { - for(let i = 2; i < process.argv.length; i++) { - const arg = process.argv[i]; - - switch(arg) { - case '--help': - doHelp(); - break; - case '--sourceMap': // bc TS uses this exact flag. esbuild... uses sourcemap (in the JS config) - case '--sourcemap': - case '--source-map': - INCLUDE_SRCMAPS = true; - break; - case '--out': - destFromArgs = process.argv[++i]; - break; - default: - if(!sourceFromArgs) { - sourceFromArgs = arg; - } else { - doHelp("Input file can only be specified once; aborting"); - } - } - } -} else { - // Display help + abort. - doHelp("Required parameters missing"); -} - -if(!sourceFromArgs || sourceFromArgs.substring(sourceFromArgs.length - 3) != '.js') { - doHelp("No input file has been specified; aborting."); -} - -const sourceFile = sourceFromArgs; -const destFile = destFromArgs || sourceFromArgs.substring(0, sourceFromArgs.length - 3) + '.wrapped.js'; - -// Now, to build the wrapper... - -const script = fs.readFileSync(sourceFile); - -// Wrapped in a function so we can leverage `const` with the result. -function buildSrcMapString() { - const sourcemapJSON = convertSourcemap.fromJSON(fs.readFileSync(`${sourceFile}.map`)).toObject(); - const encodedSrcMap = convertSourcemap.fromObject(sourcemapJSON).toBase64(); - return `//# sourceMappingURL=data:application/json;charset=utf-8;base64,${encodedSrcMap}`; -} - -// While it IS possible to do partial sourcemaps (without the sources, but with everything else) within the worker... -// the resulting sourcemaps are -surprisingly- large - larger than the code itself! -const srcMapString = INCLUDE_SRCMAPS ? buildSrcMapString() : ""; - -/* - * It'd be nice to do a 'partial' encodeURIComponent that only gets the important bits... - * but my attempts to do so end up triggering errors when loading. - */ - -let rawScript = script.toString(); -// Two layers of encoding: one for the raw source (parsed by the JS engine), -// one to 'unwrap' it from a string _within_ that source. -let jsonEncoded = JSON.stringify(rawScript); - -let wrapper = ` -// Autogenerated code. Do not modify! -// --START:LMLayerWorkerCode-- - -export var LMLayerWorkerCode = ${jsonEncoded}; - -${!INCLUDE_SRCMAPS && "// Sourcemaps have been omitted for this release build." || ''} -export var LMLayerWorkerSourcemapComment = "${srcMapString}"; - -// --END:LMLayerWorkerCode -`; - -fs.writeFileSync(destFile, wrapper); - -// Don't let there be multiples; TS may signal typing conflict if it thinks -// these are defined multiple times. -let wrapperDeclarations = ` -export const LMLayerWorkerCode: string; -export const LMLayerWorkerSourcemapComment: string; -`; - -fs.writeFileSync('build/lib/worker-main.wrapped.d.ts', wrapperDeclarations); \ No newline at end of file diff --git a/web/src/engine/predictive-text/worker-thread/build.sh b/web/src/engine/predictive-text/worker-thread/build.sh index 2e7fd9d90e5..ab788b9a588 100755 --- a/web/src/engine/predictive-text/worker-thread/build.sh +++ b/web/src/engine/predictive-text/worker-thread/build.sh @@ -34,7 +34,7 @@ builder_describe \ builder_describe_outputs \ configure /node_modules \ - build "/web/src/engine/predictive-text/worker-thread/${LIB}/worker-main.wrapped.min.js" + build "/web/src/engine/predictive-text/worker-thread/${LIB}/worker-main.min.js" builder_parse "$@" @@ -81,11 +81,12 @@ function do_build() { $INTERMEDIATE/worker-main.min.js.map \ --clean - node build-wrapper.js $INTERMEDIATE/worker-main.js \ - --out $LIB/worker-main.wrapped.js \ - --sourceMap - node build-wrapper.js $INTERMEDIATE/worker-main.min.js \ - --out $LIB/worker-main.wrapped.min.js + cp $INTERMEDIATE/worker-main.js $LIB/worker-main.js + cp $INTERMEDIATE/worker-main.js.map $LIB/worker-main.js.map + cp $INTERMEDIATE/worker-main.d.ts $LIB/worker-main.d.ts + cp $INTERMEDIATE/worker-main.min.js $LIB/worker-main.min.js + cp $INTERMEDIATE/worker-main.min.js.map $LIB/worker-main.min.js.map + # cp $INTERMEDIATE/worker-main.min.d.ts $LIB/worker-main.min.d.ts } function do_test() { diff --git a/web/src/engine/predictive-text/worker-thread/package.json b/web/src/engine/predictive-text/worker-thread/package.json index 7263f20b7c4..d5c622986ec 100644 --- a/web/src/engine/predictive-text/worker-thread/package.json +++ b/web/src/engine/predictive-text/worker-thread/package.json @@ -1,15 +1,15 @@ { "name": "@keymanapp/lm-worker", "description": "Keyman/Predictive Text worker", - "main": "./build/lib/worker-main.wrapped.js", + "main": "./build/lib/worker-main.js", "exports": { - "./worker-main.wrapped.js": { - "types": "./build/lib/worker-main.wrapped.d.ts", - "default": "./build/lib/worker-main.wrapped.js" + "./worker-main.js": { + "types": "./build/lib/worker-main.d.ts", + "default": "./build/lib/worker-main.js" }, - "./worker-main.wrapped.min.js": { - "types": "./build/lib/worker-main.wrapped.d.ts", - "default": "./build/lib/worker-main.wrapped.min.js" + "./worker-main.min.js": { + "types": "./build/lib/worker-main.d.ts", + "default": "./build/lib/worker-main.min.js" }, "./test-index": { "default": "./build/obj/test-index.js" diff --git a/web/src/engine/predictive-text/worker-thread/src/tests/test-runner/cases/worker.tests.ts b/web/src/engine/predictive-text/worker-thread/src/tests/test-runner/cases/worker.tests.ts index fcae30e7623..9aa9b450226 100644 --- a/web/src/engine/predictive-text/worker-thread/src/tests/test-runner/cases/worker.tests.ts +++ b/web/src/engine/predictive-text/worker-thread/src/tests/test-runner/cases/worker.tests.ts @@ -1,22 +1,12 @@ -import { assert } from 'chai'; -import { LMLayerWorkerCode } from "@keymanapp/lm-worker/worker-main.wrapped.js"; import { DEFAULT_BROWSER_TIMEOUT } from '@keymanapp/common-test-resources/test-timeouts.mjs'; describe('LMLayerWorker', function () { // This one makes multiple subsequent calls across the WebWorker boundary, so we should be generous here. this.timeout(DEFAULT_BROWSER_TIMEOUT); - describe('LMLayerWorkerCode', function() { - it('should exist!', function() { - assert.isString(LMLayerWorkerCode); - }); - }); - describe('Usage within a Web Worker', function () { it('should install itself in the worker context', function (done) { - let blob = new Blob([LMLayerWorkerCode], { type: 'text/javascript' }); - let uri = URL.createObjectURL(blob); - let worker = new Worker(uri); + let worker = new Worker(document.location.protocol + '//' + document.location.host + "/worker-main.js"); worker.onmessage = function thisShouldBeCalled(message) { done(); worker.terminate(); @@ -30,10 +20,9 @@ describe('LMLayerWorker', function () { }); worker.postMessage({ message: 'load', - // Since the worker's based in a blob, it's not on the 'same domain'. We need to absolute-path the model file. source: { type: 'file', - file: document.location.protocol + '//' + document.location.host + "/resources/models/simple-dummy.js" + file: "./resources/models/simple-dummy.js" } }); }); diff --git a/web/src/test/auto/headless/engine/main/inputProcessor.tests.ts b/web/src/test/auto/headless/engine/main/inputProcessor.tests.ts index d76b774d61a..ac9926467df 100644 --- a/web/src/test/auto/headless/engine/main/inputProcessor.tests.ts +++ b/web/src/test/auto/headless/engine/main/inputProcessor.tests.ts @@ -103,7 +103,7 @@ describe('InputProcessor', function() { let testDistribution: KeyDistribution = []; let keyboardWithHarness: JSKeyboardInterface; - let mainWebScriptURL = require.resolve('@keymanapp/lm-worker/worker-main.wrapped.js'); + let mainWebScriptURL = require.resolve('@keymanapp/lm-worker/worker-main.js'); // Easy peasy long context: use the unminified main script for the predictive-text worker! let coreSourceCode = fs.readFileSync(mainWebScriptURL, 'utf-8'); diff --git a/web/src/test/auto/headless/engine/main/languageProcessor.tests.ts b/web/src/test/auto/headless/engine/main/languageProcessor.tests.ts index 126feab306f..3c22f90f500 100644 --- a/web/src/test/auto/headless/engine/main/languageProcessor.tests.ts +++ b/web/src/test/auto/headless/engine/main/languageProcessor.tests.ts @@ -8,7 +8,7 @@ import path from 'node:path'; import { assert } from 'chai'; -import { SourcemappedWorker as LMWorker } from "@keymanapp/lexical-model-layer/node"; +import { NodeWorker as LMWorker } from "@keymanapp/lexical-model-layer/node"; import { LexicalModelCompiler } from '@keymanapp/kmc-model'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { SyntheticTextStore } from 'keyman/engine/keyboard'; From c90737c3c10b7423923c506038b16bd6a8ada52c Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Wed, 16 Sep 2026 16:23:58 +0200 Subject: [PATCH 2/5] refactor(web): complete unbundling of lexical model worker * Rename several classes and files to clarify difference between worker and factory that constructs worker * Rework worker factories to actually implement `WorkerFactory` interface * Fixup pathing issues where `http:/host` was returned instead of `http://host` * Rename worker-thread output from worker-main.js to worker-thread.js, resolving confusion with separate worker-main module * Add path resolution for worker-thread.js to Keyman Engine * Tweak build scripts and unit tests to work with the new unbundled paths --- common/web/types/src/lexical-model-types.ts | 4 +- web/README.md | 2 +- web/src/app/browser/build.sh | 10 ++--- web/src/app/browser/src/main.ts | 8 +++- web/src/app/webview/map-polyfill-bundler.js | 2 +- web/src/app/webview/src/main.ts | 8 +++- web/src/engine/build.sh | 2 +- web/src/engine/predictive-text/build.sh | 2 +- .../predictive-text/worker-main/src/index.ts | 13 ++++--- .../worker-main/src/lmlayer.ts | 2 +- .../worker-main/src/node/index.ts | 11 +++--- ...rker.ts => node-predictive-text-worker.ts} | 17 +++++---- .../src/node/node-worker-factory.ts | 15 ++++++++ .../worker-main/src/node/node-worker.ts | 11 ------ .../worker-main/src/web/index.ts | 11 +++--- .../worker-main/src/web/web-worker-factory.ts | 23 +++++++++++ .../worker-main/src/web/web-worker.ts | 16 -------- .../worker-main/src/worker-factory.ts | 7 ++++ .../worker-dummy-integration.tests.js | 4 +- .../headless/worker-trie-integration.tests.js | 4 +- .../cases/top-level-lmlayer.tests.ts | 33 +--------------- .../cases/worker-dummy-integration.tests.ts | 8 ++-- .../cases/worker-trie-integration.tests.ts | 8 ++-- .../predictive-text/worker-thread/build.sh | 32 ++++++++-------- .../worker-thread/package.json | 14 +++---- .../src/main/correction/execution-timer.ts | 2 +- .../main/{worker-main.ts => worker-thread.ts} | 0 .../tests/test-runner/cases/worker.tests.ts | 4 +- .../src/main/headless/inputProcessor.ts | 6 +-- .../src/main/headless/languageProcessor.ts | 6 +-- web/src/engine/src/main/keymanEngineBase.ts | 9 ++--- .../cases/gesture-processor/canary.tests.ts | 2 +- .../recordedCoordSequences.tests.ts | 2 +- .../prediction/predictionContext.tests.ts | 4 +- .../engine/main/inputProcessor.tests.ts | 38 +++++++++---------- .../engine/main/languageProcessor.tests.ts | 6 +-- web/src/test/auto/integrated/test_utils.ts | 2 +- 37 files changed, 173 insertions(+), 175 deletions(-) rename web/src/engine/predictive-text/worker-main/src/node/{mappedWorker.ts => node-predictive-text-worker.ts} (81%) create mode 100644 web/src/engine/predictive-text/worker-main/src/node/node-worker-factory.ts delete mode 100644 web/src/engine/predictive-text/worker-main/src/node/node-worker.ts create mode 100644 web/src/engine/predictive-text/worker-main/src/web/web-worker-factory.ts delete mode 100644 web/src/engine/predictive-text/worker-main/src/web/web-worker.ts create mode 100644 web/src/engine/predictive-text/worker-main/src/worker-factory.ts rename web/src/engine/predictive-text/worker-thread/src/main/{worker-main.ts => worker-thread.ts} (100%) diff --git a/common/web/types/src/lexical-model-types.ts b/common/web/types/src/lexical-model-types.ts index a3c8df1c98b..712c698b890 100644 --- a/common/web/types/src/lexical-model-types.ts +++ b/common/web/types/src/lexical-model-types.ts @@ -259,8 +259,8 @@ export interface Transform { * transition event - say, the resulting effects of neighbor keys that may * have been missed due to "fat fingering". * - * Also note that the Transform reference cannot be preserved across WebWorker - * boundaries, but this ID may. + * Also note that the Transform reference cannot be preserved across + * WebPredictiveTextWorker boundaries, but this ID may. * * This is *separate* from any LMLayer-internal identification values. */ diff --git a/web/README.md b/web/README.md index 2abc8f2b580..ad72d4b22f7 100644 --- a/web/README.md +++ b/web/README.md @@ -98,7 +98,7 @@ graph TD; Gestures["/web/src/engine/gesture-processor"]; Gestures-->WebUtils; - subgraph PredText["PredText: WebWorker + its interface"] + subgraph PredText["PredText: WebPredictiveTextWorker + its interface"] LMLayer; LMWorker; Models; diff --git a/web/src/app/browser/build.sh b/web/src/app/browser/build.sh index 3a3dfbf6150..dd72d621987 100755 --- a/web/src/app/browser/build.sh +++ b/web/src/app/browser/build.sh @@ -72,13 +72,13 @@ compile_and_copy() { mkdir -p "$KEYMAN_ROOT/web/build/app/resources/osk" cp -R "$KEYMAN_ROOT/web/src/resources/osk/." "$KEYMAN_ROOT/web/build/app/resources/osk/" - cp "${KEYMAN_ROOT}/web/src/engine/predictive-text/worker-thread/build/lib/worker-main.js" "${BUILD_ROOT}/debug/worker-main.js" - cp "${KEYMAN_ROOT}/web/src/engine/predictive-text/worker-thread/build/lib/worker-main.js.map" "${BUILD_ROOT}/debug/worker-main.js.map" - cp "${KEYMAN_ROOT}/web/src/engine/predictive-text/worker-thread/build/lib/worker-main.d.ts" "${BUILD_ROOT}/debug/worker-main.d.ts" + cp "${KEYMAN_ROOT}/web/src/engine/predictive-text/worker-thread/build/lib/worker-thread.js" "${BUILD_ROOT}/debug/worker-thread.js" + cp "${KEYMAN_ROOT}/web/src/engine/predictive-text/worker-thread/build/lib/worker-thread.js.map" "${BUILD_ROOT}/debug/worker-thread.js.map" + cp "${KEYMAN_ROOT}/web/src/engine/predictive-text/worker-thread/build/lib/worker-thread.d.ts" "${BUILD_ROOT}/debug/worker-thread.d.ts" # TODO: rename to lm-worker.js? do we keep debug/release distinction through .js vs .min.js or via folder? - cp "${KEYMAN_ROOT}/web/src/engine/predictive-text/worker-thread/build/lib/worker-main.min.js" "${BUILD_ROOT}/release/worker-main.js" - cp "${KEYMAN_ROOT}/web/src/engine/predictive-text/worker-thread/build/lib/worker-main.min.js.map" "${BUILD_ROOT}/release/worker-main.js.map" + cp "${KEYMAN_ROOT}/web/src/engine/predictive-text/worker-thread/build/lib/worker-thread.min.js" "${BUILD_ROOT}/release/worker-thread.js" + cp "${KEYMAN_ROOT}/web/src/engine/predictive-text/worker-thread/build/lib/worker-thread.min.js.map" "${BUILD_ROOT}/release/worker-thread.js.map" # Update the build/publish copy of our build artifacts prepare diff --git a/web/src/app/browser/src/main.ts b/web/src/app/browser/src/main.ts index 3e3a168ccd9..65441509667 100644 --- a/web/src/app/browser/src/main.ts +++ b/web/src/app/browser/src/main.ts @@ -1,5 +1,9 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + import { KeymanEngine } from './keymanEngine.js' -import { WebWorker } from '@keymanapp/lexical-model-layer/web' +import { webPredictiveTextWorkerFactory } from '@keymanapp/lexical-model-layer/web' /** * Determine path and protocol of executing script, setting them as @@ -9,4 +13,4 @@ const ss = (document.currentScript as HTMLScriptElement)?.src; const sPath = ss ? ss.substring(0, ss.lastIndexOf('/') + 1) : './'; // @ts-ignore -window['keyman'] = new KeymanEngine(WebWorker, sPath); \ No newline at end of file +window['keyman'] = new KeymanEngine(webPredictiveTextWorkerFactory, sPath); \ No newline at end of file diff --git a/web/src/app/webview/map-polyfill-bundler.js b/web/src/app/webview/map-polyfill-bundler.js index e4d25867392..393efa296fa 100644 --- a/web/src/app/webview/map-polyfill-bundler.js +++ b/web/src/app/webview/map-polyfill-bundler.js @@ -20,7 +20,7 @@ const commonConfig = { sourceRoot: '@keymanapp/keyman/web/build/app/webview/debug/' }; -await prepareTslibTreeshaking(commonConfig, /worker-main(?:\.min)?\.js/); +await prepareTslibTreeshaking(commonConfig, /worker-thread(?:\.min)?\.js/); await esbuild.build(commonConfig); diff --git a/web/src/app/webview/src/main.ts b/web/src/app/webview/src/main.ts index 3e3a168ccd9..65441509667 100644 --- a/web/src/app/webview/src/main.ts +++ b/web/src/app/webview/src/main.ts @@ -1,5 +1,9 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + import { KeymanEngine } from './keymanEngine.js' -import { WebWorker } from '@keymanapp/lexical-model-layer/web' +import { webPredictiveTextWorkerFactory } from '@keymanapp/lexical-model-layer/web' /** * Determine path and protocol of executing script, setting them as @@ -9,4 +13,4 @@ const ss = (document.currentScript as HTMLScriptElement)?.src; const sPath = ss ? ss.substring(0, ss.lastIndexOf('/') + 1) : './'; // @ts-ignore -window['keyman'] = new KeymanEngine(WebWorker, sPath); \ No newline at end of file +window['keyman'] = new KeymanEngine(webPredictiveTextWorkerFactory, sPath); \ No newline at end of file diff --git a/web/src/engine/build.sh b/web/src/engine/build.sh index 70150fe681c..121819b990d 100755 --- a/web/src/engine/build.sh +++ b/web/src/engine/build.sh @@ -67,7 +67,7 @@ run_tests() { # "TypeError [ERR_INVALID_URL_SCHEME]: The URL must be of scheme file" # when c8 tries to create a report from the raw coverage data. The reason # is a URL starting with `data:text/javascript` coming from - # `web/src/engine/predictive-text/worker-main/src/node/mappedWorker.ts`. + # `web/src/engine/predictive-text/worker-main/src/node/node-predictive-text-worker.ts`. # # So we first run all javascript tests except languageProcessor.tests.js # with coverage, and then in a second step run languageProcessor.tests.js diff --git a/web/src/engine/predictive-text/build.sh b/web/src/engine/predictive-text/build.sh index c73cc2da93a..e63963448f8 100755 --- a/web/src/engine/predictive-text/build.sh +++ b/web/src/engine/predictive-text/build.sh @@ -32,7 +32,7 @@ builder_describe_outputs \ build:templates "/web/src/engine/predictive-text/templates/build/obj/index.js" \ build:wordbreakers "/web/src/engine/predictive-text/wordbreakers/build/main/obj/index.js" \ build:worker-main "/web/src/engine/predictive-text/worker-main/build/lib/index.mjs" \ - build:worker-thread "/web/src/engine/predictive-text/worker-thread/build/lib/worker-main.js" + build:worker-thread "/web/src/engine/predictive-text/worker-thread/build/lib/worker-thread.js" #### Build action definitions #### diff --git a/web/src/engine/predictive-text/worker-main/src/index.ts b/web/src/engine/predictive-text/worker-main/src/index.ts index bdee0489174..98685e8c494 100644 --- a/web/src/engine/predictive-text/worker-main/src/index.ts +++ b/web/src/engine/predictive-text/worker-main/src/index.ts @@ -1,7 +1,8 @@ -export { LMLayer } from './lmlayer.js'; -export { NodeWorker } from './node/node-worker.js'; -export { WebWorker } from './web/web-worker.js'; +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ -export interface WorkerFactory { - constructInstance(): Worker -} \ No newline at end of file +export { LMLayer } from './lmlayer.js'; +export { type WorkerFactory } from './worker-factory.js'; +export { nodePredictiveTextWorkerFactory, NodePredictiveTextWorkerFactory } from './node/node-worker-factory.js'; +export { webPredictiveTextWorkerFactory, WebPredictiveTextWorkerFactory } from './web/web-worker-factory.js'; diff --git a/web/src/engine/predictive-text/worker-main/src/lmlayer.ts b/web/src/engine/predictive-text/worker-main/src/lmlayer.ts index 46d0aa7c936..b9ea6b721f0 100644 --- a/web/src/engine/predictive-text/worker-main/src/lmlayer.ts +++ b/web/src/engine/predictive-text/worker-main/src/lmlayer.ts @@ -230,7 +230,7 @@ export class LMLayer { /** * Clears out any computational resources in use by the LMLayer, including shutting - * down any internal WebWorkers. + * down any internal PredictiveTextWorkers. */ public shutdown() { this._worker.terminate(); diff --git a/web/src/engine/predictive-text/worker-main/src/node/index.ts b/web/src/engine/predictive-text/worker-main/src/node/index.ts index d9156a9ef5b..db8afac52be 100644 --- a/web/src/engine/predictive-text/worker-main/src/node/index.ts +++ b/web/src/engine/predictive-text/worker-main/src/node/index.ts @@ -1,6 +1,7 @@ -export { LMLayer } from '../lmlayer.js'; -export { NodeWorker } from './node-worker.js'; +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ -export interface WorkerFactory { - constructInstance(): Worker - } \ No newline at end of file +export { LMLayer } from '../lmlayer.js'; +export { type WorkerFactory } from '../worker-factory.js'; +export { nodePredictiveTextWorkerFactory, NodePredictiveTextWorkerFactory } from './node-worker-factory.js'; diff --git a/web/src/engine/predictive-text/worker-main/src/node/mappedWorker.ts b/web/src/engine/predictive-text/worker-main/src/node/node-predictive-text-worker.ts similarity index 81% rename from web/src/engine/predictive-text/worker-main/src/node/mappedWorker.ts rename to web/src/engine/predictive-text/worker-main/src/node/node-predictive-text-worker.ts index a2acbd68da4..b450ebd7d23 100644 --- a/web/src/engine/predictive-text/worker-main/src/node/mappedWorker.ts +++ b/web/src/engine/predictive-text/worker-main/src/node/node-predictive-text-worker.ts @@ -4,8 +4,13 @@ // Defines types related to Node workers. import * as worker from 'node:worker_threads'; +import * as path from 'node:path'; import { Buffer } from 'node:buffer'; -import { URL } from 'node:url'; +import { fileURLToPath, URL } from 'node:url'; + +const __filename__ = fileURLToPath(import.meta.url); +const __dirname__ = path.dirname(__filename__); +const workerThreadFilename = __dirname__ + "/../../../../worker-thread/build/lib/worker-thread.js"; /** * Defines mappings from Node Worker signatures to WebWorker signatures @@ -46,26 +51,22 @@ self.postMessage = postMessage; self.importScripts = importScripts; self.self = self; // make it global! // Start off by importing the main worker itself -// importScripts('${import.meta.dirname}/../../../worker-thread/build/lib/worker-main.js'); -console.dir(import.meta); -importScripts('${import.meta.dirname}/worker-main.js'); +importScripts(${JSON.stringify(workerThreadFilename)}); `; - - /** * Uses the Node version of Workers to provide proper, authentic separate-thread * 'sandboxing'. Also intercepts and interprets certain WebWorker method signatures * necessary to run the WebWorker-oriented worker code. */ -export class MappedWorker extends worker.Worker implements Worker { +export class NodePredictiveTextWorker extends worker.Worker implements Worker { constructor() { const buffer = Buffer.from(nodeWorkerToWebWorkerMappingSource); const dataSrc = "data:text/javascript;base64," + buffer.toString('base64'); //@ts-ignore super(new URL(dataSrc)); - // WebWorkers have a defined `onmessage` function, rather than this.on('message', ...) + // Workers have a defined `onmessage` function, rather than this.on('message', ...) this.on('message', (ev) => { if(this.onmessage) { this.onmessage({data: ev[0]}); diff --git a/web/src/engine/predictive-text/worker-main/src/node/node-worker-factory.ts b/web/src/engine/predictive-text/worker-main/src/node/node-worker-factory.ts new file mode 100644 index 00000000000..e0f1821d939 --- /dev/null +++ b/web/src/engine/predictive-text/worker-main/src/node/node-worker-factory.ts @@ -0,0 +1,15 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ +import { WorkerFactory } from "../worker-factory.js"; +import { NodePredictiveTextWorker } from "./node-predictive-text-worker.js"; + +export class NodePredictiveTextWorkerFactory implements WorkerFactory { + constructInstance(_rootPath?: string): Worker { + // note: rootPath is never used for Node worker, as it is only instanatiated + // in unit tests, where paths are known + return new NodePredictiveTextWorker(); + } +} + +export const nodePredictiveTextWorkerFactory = new NodePredictiveTextWorkerFactory(); diff --git a/web/src/engine/predictive-text/worker-main/src/node/node-worker.ts b/web/src/engine/predictive-text/worker-main/src/node/node-worker.ts deleted file mode 100644 index 8d18badf0de..00000000000 --- a/web/src/engine/predictive-text/worker-main/src/node/node-worker.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Keyman is copyright (C) SIL Global. MIT License. - */ -import { MappedWorker } from "./mappedWorker.js"; - -// TODO: eliminate MappedWorker as its own thing -export class NodeWorker { - static constructInstance(): Worker { - return new MappedWorker(); - } -} \ No newline at end of file diff --git a/web/src/engine/predictive-text/worker-main/src/web/index.ts b/web/src/engine/predictive-text/worker-main/src/web/index.ts index 25bdb2522ff..76d84fee0a6 100644 --- a/web/src/engine/predictive-text/worker-main/src/web/index.ts +++ b/web/src/engine/predictive-text/worker-main/src/web/index.ts @@ -1,6 +1,7 @@ -export { LMLayer } from '../lmlayer.js'; -export { WebWorker } from './web-worker.js'; +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ -export interface WorkerFactory { - constructInstance(): Worker -} +export { LMLayer } from '../lmlayer.js'; +export { type WorkerFactory } from '../worker-factory.js'; +export { webPredictiveTextWorkerFactory, WebPredictiveTextWorkerFactory } from './web-worker-factory.js'; diff --git a/web/src/engine/predictive-text/worker-main/src/web/web-worker-factory.ts b/web/src/engine/predictive-text/worker-main/src/web/web-worker-factory.ts new file mode 100644 index 00000000000..f1aeedcca99 --- /dev/null +++ b/web/src/engine/predictive-text/worker-main/src/web/web-worker-factory.ts @@ -0,0 +1,23 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +import { WorkerFactory } from "../worker-factory.js"; + +export class WebPredictiveTextWorkerFactory implements WorkerFactory { + constructInstance(rootPath?: string): Worker { + if(!rootPath) { + rootPath = './'; + } + else if(!rootPath.endsWith('/')) { + rootPath = rootPath + '/'; + } + return new Worker(rootPath + 'worker-thread.js'); + } + + constructTestInstance(): Worker { + return new Worker('/web/src/engine/predictive-text/worker-thread/build/lib/worker-thread.js'); + } +} + +export const webPredictiveTextWorkerFactory = new WebPredictiveTextWorkerFactory(); diff --git a/web/src/engine/predictive-text/worker-main/src/web/web-worker.ts b/web/src/engine/predictive-text/worker-main/src/web/web-worker.ts deleted file mode 100644 index c55a5cd630f..00000000000 --- a/web/src/engine/predictive-text/worker-main/src/web/web-worker.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Keyman is copyright (C) SIL Global. MIT License. - */ - -export class WebWorker { - static constructInstance(): Worker { - return new Worker(this.workerURI()); - } - - static workerURI(): string { - // TODO: worker-thread generates worker-main.js,... whaaa - // TODO: worker-main.min.js? - // TODO: paths - return './worker-main.js'; - } -} \ No newline at end of file diff --git a/web/src/engine/predictive-text/worker-main/src/worker-factory.ts b/web/src/engine/predictive-text/worker-main/src/worker-factory.ts new file mode 100644 index 00000000000..7f40f2a4669 --- /dev/null +++ b/web/src/engine/predictive-text/worker-main/src/worker-factory.ts @@ -0,0 +1,7 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +export interface WorkerFactory { + constructInstance(rootPath?: string): Worker; +}; \ No newline at end of file diff --git a/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-dummy-integration.tests.js b/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-dummy-integration.tests.js index 7fcce07e5db..b5477e22489 100644 --- a/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-dummy-integration.tests.js +++ b/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-dummy-integration.tests.js @@ -4,7 +4,7 @@ import fs from 'fs'; import { createRequire } from 'module'; const require = createRequire(import.meta.url); -import { LMLayer, NodeWorker as Worker } from '#./node/index.js'; +import { LMLayer, nodePredictiveTextWorkerFactory } from '#./node/index.js'; import { capabilities, iGotDistractedByHazel } from '@keymanapp/common-test-resources/model-helpers.mjs'; /* @@ -20,7 +20,7 @@ describe('LMLayer using dummy model', function () { let worker; beforeEach(function() { - worker = Worker.constructInstance(); + worker = nodePredictiveTextWorkerFactory.constructInstance(); lmLayer = new LMLayer(capabilities(), worker); }); diff --git a/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-trie-integration.tests.js b/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-trie-integration.tests.js index aa248a3c443..05716f5c7ab 100644 --- a/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-trie-integration.tests.js +++ b/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-trie-integration.tests.js @@ -3,7 +3,7 @@ import { assert } from 'chai'; import { createRequire } from 'module'; const require = createRequire(import.meta.url); -import { LMLayer, NodeWorker as Worker } from '#./node/index.js'; +import { LMLayer, nodePredictiveTextWorkerFactory } from '#./node/index.js'; import { capabilities } from '@keymanapp/common-test-resources/model-helpers.mjs'; /* @@ -14,7 +14,7 @@ describe('LMLayer using the trie model', function () { let worker; beforeEach(function() { - worker = Worker.constructInstance(); + worker = nodePredictiveTextWorkerFactory.constructInstance(); lmLayer = new LMLayer(capabilities(), worker, true); }); diff --git a/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/top-level-lmlayer.tests.ts b/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/top-level-lmlayer.tests.ts index df5cb45e3bc..cab08232c81 100644 --- a/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/top-level-lmlayer.tests.ts +++ b/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/top-level-lmlayer.tests.ts @@ -1,6 +1,6 @@ import { assert } from 'chai'; -import { LMLayer, WebWorker } from "@keymanapp/lexical-model-layer/web"; +import { LMLayer, webPredictiveTextWorkerFactory } from "@keymanapp/lexical-model-layer/web"; import { DEFAULT_BROWSER_TIMEOUT } from '@keymanapp/common-test-resources/test-timeouts.mjs'; import { defaultCapabilities } from '../helpers.mjs'; @@ -10,38 +10,9 @@ describe('LMLayer', function () { describe('[[constructor]]', function () { it('should construct with a single argument', function () { - let lmLayer = new LMLayer(defaultCapabilities, WebWorker.constructInstance(), true); + let lmLayer = new LMLayer(defaultCapabilities, webPredictiveTextWorkerFactory.constructTestInstance(), true); assert.instanceOf(lmLayer, LMLayer); lmLayer.shutdown(); }); }); - - describe('#asBlobURI()', function () { - // #asBlobURI() requires browser APIs, hence why it cannot be tested headless in Node. - it('should take a function and convert it into a blob function', function (done) { - function dummyHandler() { - // Post something weird, so we can be reasonably certain the Web Worker is... - // well, working. - // WARNING: Do NOT refactor this string as a variable. It **MUST** remain a string - // in this function body, because the code in this function's body gets - // stringified! - postMessage('fhqwhgads'); - } - - // Note: the full declaration exists; the code we want is wrapped within the func. - // So... let's just call the func. - const workerSrc = dummyHandler.toString() + "\ndummyHandler()"; - let uri = WebWorker.asBlobURI(workerSrc); - assert.match(uri, /^blob:/); - - let worker = new Worker(uri); - worker.onmessage = function thisShouldBeCalled(event) { - assert.propertyVal(event, 'data', 'fhqwhgads'); - worker.terminate(); - done(); - }; - - worker.postMessage('test'); - }) - }) }); diff --git a/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/worker-dummy-integration.tests.ts b/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/worker-dummy-integration.tests.ts index 04af7ec8dcc..4ce3d0489af 100644 --- a/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/worker-dummy-integration.tests.ts +++ b/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/worker-dummy-integration.tests.ts @@ -1,6 +1,6 @@ import { assert } from 'chai'; -import { LMLayer, WebWorker } from "@keymanapp/lexical-model-layer/web"; +import { LMLayer, webPredictiveTextWorkerFactory } from "@keymanapp/lexical-model-layer/web"; import { DEFAULT_BROWSER_TIMEOUT } from '@keymanapp/common-test-resources/test-timeouts.mjs'; import { defaultCapabilities } from '../helpers.mjs'; @@ -28,7 +28,7 @@ describe('LMLayer using dummy model', function () { let loc = document.location; // config.testFile generally starts with a '/', with the path resembling the actual full local // filesystem for the drive. - domain = `${loc.protocol}/${loc.host}` + domain = `${loc.protocol}//${loc.host}`; // Test-config setups will take care of the rest; the server-path will be rooted at the repo root. // With aliasing for resources/. @@ -52,7 +52,7 @@ describe('LMLayer using dummy model', function () { describe('Prediction', function () { it('will predict future suggestions', function () { - var lmLayer = new LMLayer(defaultCapabilities, WebWorker.constructInstance(), true); + var lmLayer = new LMLayer(defaultCapabilities, webPredictiveTextWorkerFactory.constructTestInstance(), true); var stripIDs = function(suggestions) { suggestions.forEach(function(suggestion) { @@ -93,7 +93,7 @@ describe('LMLayer using dummy model', function () { describe('Wordbreaking', function () { it('will perform (default) wordbreaking and return word at caret', function () { - var lmLayer = new LMLayer(defaultCapabilities, WebWorker.constructInstance()); + var lmLayer = new LMLayer(defaultCapabilities, webPredictiveTextWorkerFactory.constructTestInstance()); // We're testing many as asynchronous messages in a row. // this would be cleaner using async/await syntax, but diff --git a/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/worker-trie-integration.tests.ts b/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/worker-trie-integration.tests.ts index bf8d0aef5c8..8f266e20260 100644 --- a/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/worker-trie-integration.tests.ts +++ b/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/worker-trie-integration.tests.ts @@ -1,6 +1,6 @@ import { assert } from 'chai'; -import { LMLayer, WebWorker } from "@keymanapp/lexical-model-layer/web"; +import { LMLayer, webPredictiveTextWorkerFactory } from "@keymanapp/lexical-model-layer/web"; import { DEFAULT_BROWSER_TIMEOUT } from '@keymanapp/common-test-resources/test-timeouts.mjs'; import { defaultCapabilities } from '../helpers.mjs'; @@ -17,7 +17,7 @@ describe('LMLayer using the trie model', function () { before(async () => { let loc = document.location; - domain = `${loc.protocol}/${loc.host}`; + domain = `${loc.protocol}//${loc.host}`; // Test-config setups will take care of the rest; the server-path will be rooted at the repo root. // With aliasing for resources/. @@ -29,7 +29,7 @@ describe('LMLayer using the trie model', function () { // Parameter 3 = true: enables 'test mode', disables correction-search timeout. // This helps prevent the correction-search timeout from flaking out periodically during unit tests in // CI, since remote servers / devices are involved. - var lmLayer = new LMLayer(defaultCapabilities, WebWorker.constructInstance(), true); + var lmLayer = new LMLayer(defaultCapabilities, webPredictiveTextWorkerFactory.constructTestInstance(), true); // We're testing many as asynchronous messages in a row. // this would be cleaner using async/await syntax, but @@ -71,7 +71,7 @@ describe('LMLayer using the trie model', function () { // // https://community.software.sil.org/t/search-term-to-key-in-lexical-model-not-working-both-ways-by-default/3133 it('should use the default searchTermToKey()', function () { - var lmLayer = new LMLayer(defaultCapabilities, WebWorker.constructInstance(), /* testMode */ true); + var lmLayer = new LMLayer(defaultCapabilities, webPredictiveTextWorkerFactory.constructTestInstance(), /* testMode */ true); let loc = document.location; return lmLayer.loadModel( diff --git a/web/src/engine/predictive-text/worker-thread/build.sh b/web/src/engine/predictive-text/worker-thread/build.sh index ab788b9a588..31c7ac4b586 100755 --- a/web/src/engine/predictive-text/worker-thread/build.sh +++ b/web/src/engine/predictive-text/worker-thread/build.sh @@ -34,7 +34,7 @@ builder_describe \ builder_describe_outputs \ configure /node_modules \ - build "/web/src/engine/predictive-text/worker-thread/${LIB}/worker-main.min.js" + build "/web/src/engine/predictive-text/worker-thread/${LIB}/worker-thread.min.js" builder_parse "$@" @@ -48,7 +48,7 @@ function do_configure() { function do_build() { # Declaration bundling. - tsc --emitDeclarationOnly --outFile $INTERMEDIATE/worker-main.d.ts + tsc --emitDeclarationOnly --outFile $INTERMEDIATE/worker-thread.d.ts # Some automated tests currently rely upon the individual output files. tsc @@ -57,19 +57,19 @@ function do_build() { # The ES6 target needs no polyfills - we go straight to the wrapped version. - node_es_bundle src/main/worker-main.ts \ - --out $INTERMEDIATE/worker-main.js \ + node_es_bundle src/main/worker-thread.ts \ + --out $INTERMEDIATE/worker-thread.js \ --charset "utf8" \ --target "es6" \ --sourceRoot '@keymanapp/keyman/web/src/engine/predictive-text/worker-thread/src/main' node "$SRCMAP_CLEANER" \ - $INTERMEDIATE/worker-main.js.map \ - $INTERMEDIATE/worker-main.js.map \ + $INTERMEDIATE/worker-thread.js.map \ + $INTERMEDIATE/worker-thread.js.map \ --clean - node_es_bundle src/main/worker-main.ts \ - --out $INTERMEDIATE/worker-main.min.js \ + node_es_bundle src/main/worker-thread.ts \ + --out $INTERMEDIATE/worker-thread.min.js \ --minify \ --charset "utf8" \ --profile build/filesize-profile.log \ @@ -77,16 +77,16 @@ function do_build() { --sourceRoot '@keymanapp/keyman/web/src/engine/predictive-text/worker-thread/src/main' node "$SRCMAP_CLEANER" \ - $INTERMEDIATE/worker-main.min.js.map \ - $INTERMEDIATE/worker-main.min.js.map \ + $INTERMEDIATE/worker-thread.min.js.map \ + $INTERMEDIATE/worker-thread.min.js.map \ --clean - cp $INTERMEDIATE/worker-main.js $LIB/worker-main.js - cp $INTERMEDIATE/worker-main.js.map $LIB/worker-main.js.map - cp $INTERMEDIATE/worker-main.d.ts $LIB/worker-main.d.ts - cp $INTERMEDIATE/worker-main.min.js $LIB/worker-main.min.js - cp $INTERMEDIATE/worker-main.min.js.map $LIB/worker-main.min.js.map - # cp $INTERMEDIATE/worker-main.min.d.ts $LIB/worker-main.min.d.ts + cp $INTERMEDIATE/worker-thread.js $LIB/worker-thread.js + cp $INTERMEDIATE/worker-thread.js.map $LIB/worker-thread.js.map + cp $INTERMEDIATE/worker-thread.d.ts $LIB/worker-thread.d.ts + cp $INTERMEDIATE/worker-thread.min.js $LIB/worker-thread.min.js + cp $INTERMEDIATE/worker-thread.min.js.map $LIB/worker-thread.min.js.map + # cp $INTERMEDIATE/worker-thread.min.d.ts $LIB/worker-thread.min.d.ts } function do_test() { diff --git a/web/src/engine/predictive-text/worker-thread/package.json b/web/src/engine/predictive-text/worker-thread/package.json index d5c622986ec..a1808e9fe52 100644 --- a/web/src/engine/predictive-text/worker-thread/package.json +++ b/web/src/engine/predictive-text/worker-thread/package.json @@ -1,15 +1,15 @@ { "name": "@keymanapp/lm-worker", "description": "Keyman/Predictive Text worker", - "main": "./build/lib/worker-main.js", + "main": "./build/lib/worker-thread.js", "exports": { - "./worker-main.js": { - "types": "./build/lib/worker-main.d.ts", - "default": "./build/lib/worker-main.js" + "./worker-thread.js": { + "types": "./build/lib/worker-thread.d.ts", + "default": "./build/lib/worker-thread.js" }, - "./worker-main.min.js": { - "types": "./build/lib/worker-main.d.ts", - "default": "./build/lib/worker-main.min.js" + "./worker-thread.min.js": { + "types": "./build/lib/worker-thread.d.ts", + "default": "./build/lib/worker-thread.min.js" }, "./test-index": { "default": "./build/obj/test-index.js" diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/execution-timer.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/execution-timer.ts index 1ba6f535a00..21d42b2f961 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/execution-timer.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/execution-timer.ts @@ -369,7 +369,7 @@ export class ExecutionTimer { // By using the state field, we'll get an error if we somehow call other // class methods during the defer. this.activeSpan = new ExecutionSpan(this.deferBucket, () => this.activeSpan = null); - // WebWorker messages appear to come in via the macrotask queue. + // Worker messages appear to come in via the macrotask queue. await timedPromise(minWait); this.activeSpan.end(); diff --git a/web/src/engine/predictive-text/worker-thread/src/main/worker-main.ts b/web/src/engine/predictive-text/worker-thread/src/main/worker-thread.ts similarity index 100% rename from web/src/engine/predictive-text/worker-thread/src/main/worker-main.ts rename to web/src/engine/predictive-text/worker-thread/src/main/worker-thread.ts diff --git a/web/src/engine/predictive-text/worker-thread/src/tests/test-runner/cases/worker.tests.ts b/web/src/engine/predictive-text/worker-thread/src/tests/test-runner/cases/worker.tests.ts index 9aa9b450226..45a95c8a292 100644 --- a/web/src/engine/predictive-text/worker-thread/src/tests/test-runner/cases/worker.tests.ts +++ b/web/src/engine/predictive-text/worker-thread/src/tests/test-runner/cases/worker.tests.ts @@ -1,12 +1,12 @@ import { DEFAULT_BROWSER_TIMEOUT } from '@keymanapp/common-test-resources/test-timeouts.mjs'; describe('LMLayerWorker', function () { - // This one makes multiple subsequent calls across the WebWorker boundary, so we should be generous here. + // This one makes multiple subsequent calls across the PredictiveTextWorker boundary, so we should be generous here. this.timeout(DEFAULT_BROWSER_TIMEOUT); describe('Usage within a Web Worker', function () { it('should install itself in the worker context', function (done) { - let worker = new Worker(document.location.protocol + '//' + document.location.host + "/worker-main.js"); + let worker = new Worker('/engine/predictive-text/worker-thread/build/lib/worker-thread.js'); worker.onmessage = function thisShouldBeCalled(message) { done(); worker.terminate(); diff --git a/web/src/engine/src/main/headless/inputProcessor.ts b/web/src/engine/src/main/headless/inputProcessor.ts index 5018e4de02b..f21d76964ad 100644 --- a/web/src/engine/src/main/headless/inputProcessor.ts +++ b/web/src/engine/src/main/headless/inputProcessor.ts @@ -27,7 +27,7 @@ import { import { TranscriptionCache } from "./transcriptionCache.js"; import { LexicalModelTypes } from '@keymanapp/common-types'; -import { WorkerFactory } from "@keymanapp/lexical-model-layer"; +import { type WorkerFactory } from "@keymanapp/lexical-model-layer"; // Only consider raw-insertion transforms. Delete-left and delete-right disqualify an // incoming transform from reverting post-suggestion whitespace (or similar). @@ -53,7 +53,7 @@ export class InputProcessor { private readonly contextCache = new TranscriptionCache(); - constructor(device: DeviceSpec, predictiveWorkerFactory: WorkerFactory, options: ProcessorInitOptions) { + constructor(device: DeviceSpec, sourcePath: string, predictiveWorkerFactory: WorkerFactory, options: ProcessorInitOptions) { if(!device) { throw new Error('device must be defined'); } @@ -61,7 +61,7 @@ export class InputProcessor { this.contextDevice = device; this.jsKbdProcessor = new JSKeyboardProcessor(device, options); this._keyboardProcessor = this.jsKbdProcessor; - this._languageProcessor = new LanguageProcessor(predictiveWorkerFactory, this.contextCache); + this._languageProcessor = new LanguageProcessor(predictiveWorkerFactory, sourcePath, this.contextCache); } public async init(paths: PathConfiguration, storeSerializer: VariableStoreSerializer): Promise { diff --git a/web/src/engine/src/main/headless/languageProcessor.ts b/web/src/engine/src/main/headless/languageProcessor.ts index 20515c22885..960a8ad1e2e 100644 --- a/web/src/engine/src/main/headless/languageProcessor.ts +++ b/web/src/engine/src/main/headless/languageProcessor.ts @@ -1,5 +1,5 @@ import { EventEmitter } from "eventemitter3"; -import { LMLayer, WorkerFactory } from "@keymanapp/lexical-model-layer/web"; +import { LMLayer, type WorkerFactory } from "@keymanapp/lexical-model-layer/web"; import { Transcription, TextStoreLanguageProcessorInterface, SyntheticTextStore, ProcessorAction } from 'keyman/engine/keyboard'; import { LanguageProcessorEventMap, ModelSpec, StateChangeEnum, ReadySuggestions } from 'keyman/engine/interfaces'; import { ContextWindow } from "./contextWindow.js"; @@ -25,7 +25,7 @@ export class LanguageProcessor extends EventEmitter { private _state: StateChangeEnum = 'inactive'; - public constructor(predictiveWorkerFactory: WorkerFactory, transcriptionCache: TranscriptionCache, supportsRightDeletions: boolean = false) { + public constructor(predictiveWorkerFactory: WorkerFactory, sourcePath: string, transcriptionCache: TranscriptionCache, supportsRightDeletions: boolean = false) { super(); this.recentTranscriptions = transcriptionCache; @@ -44,7 +44,7 @@ export class LanguageProcessor extends EventEmitter { let workerInstance: Worker; try { - workerInstance = predictiveWorkerFactory?.constructInstance(); + workerInstance = predictiveWorkerFactory?.constructInstance(sourcePath); } catch(e) { // We can condition on `lmEngine` being null/undefined. console.warn('Web workers are not available: ' + (e ?? '').toString()); diff --git a/web/src/engine/src/main/keymanEngineBase.ts b/web/src/engine/src/main/keymanEngineBase.ts index 2ee22a2b370..aa74ca85c41 100644 --- a/web/src/engine/src/main/keymanEngineBase.ts +++ b/web/src/engine/src/main/keymanEngineBase.ts @@ -4,7 +4,7 @@ import { type KeyEvent, JSKeyboard, Keyboard, KeyboardProperties, KeyboardKeymanGlobal, ProcessorAction } from "keyman/engine/keyboard"; import { ProcessorInitOptions } from 'keyman/engine/js-processor'; import { DOMKeyboardLoader } from "keyman/engine/keyboard"; -import { WorkerFactory } from "@keymanapp/lexical-model-layer/web" +import { type WorkerFactory } from "@keymanapp/lexical-model-layer/web" import { InputProcessor } from './headless/inputProcessor.js'; import { OSKView } from "keyman/engine/osk"; import { KeyboardRequisitioner, ModelCache, toUnprefixedKeyboardId, DOMCloudRequester } from "keyman/engine/keyboard-storage"; @@ -107,10 +107,7 @@ export class KeymanEngineBase< }; /** - * @param worker A configured WebWorker to serve as the predictive-text engine's main thread. - * Available in the following variants: - * - sourcemapped, unminified (debug) - * - non-sourcemapped + minified (release) + * @param worker A factory that provides a Worker that serves as the predictive-text engine's main thread. * @param config * @param contextManager * @param processorConfigInitializer A one-time use closure used to initialize certain critical components reliant @@ -129,7 +126,7 @@ export class KeymanEngineBase< const processorConfiguration = processorConfigInitializer(this); processorConfiguration.baseLayout = determineBaseLayout(); this.interface = processorConfiguration.keyboardInterface as KeyboardInterfaceBase; - this.core = new InputProcessor(config.hostDevice, workerFactory, processorConfiguration); + this.core = new InputProcessor(config.hostDevice, config.sourcePath, workerFactory, processorConfiguration); this.core.languageProcessor.on('statechange', (state) => { // The banner controller cannot directly trigger a layout-refresh at this time, diff --git a/web/src/test/auto/dom/cases/gesture-processor/canary.tests.ts b/web/src/test/auto/dom/cases/gesture-processor/canary.tests.ts index 7c3fde9b85a..fe030396d04 100644 --- a/web/src/test/auto/dom/cases/gesture-processor/canary.tests.ts +++ b/web/src/test/auto/dom/cases/gesture-processor/canary.tests.ts @@ -22,7 +22,7 @@ describe("'Canary' checks", function() { let loc = document.location; // config.testFile generally starts with a '/', with the path resembling the actual full local // filesystem for the drive. - domain = `${loc.protocol}/${loc.host}` + domain = `${loc.protocol}//${loc.host}`; // Test-config setups will take care of the rest; the server-path will be rooted at the repo root. // With aliasing for resources/. diff --git a/web/src/test/auto/dom/cases/gesture-processor/recordedCoordSequences.tests.ts b/web/src/test/auto/dom/cases/gesture-processor/recordedCoordSequences.tests.ts index 61da5cd09c7..7a7a9a69c6f 100644 --- a/web/src/test/auto/dom/cases/gesture-processor/recordedCoordSequences.tests.ts +++ b/web/src/test/auto/dom/cases/gesture-processor/recordedCoordSequences.tests.ts @@ -17,7 +17,7 @@ function isOnAndroid() { const loc = document.location; // config.testFile generally starts with a '/', with the path resembling the actual full local // filesystem for the drive. -const domain = `${loc.protocol}/${loc.host}` +const domain = `${loc.protocol}//${loc.host}`; async function fetchRecording(jsonFilename: string): Promise { const jsonResponse = await fetch(new URL(`${domain}/resources/json/${jsonFilename}.json`)); diff --git a/web/src/test/auto/headless/engine/interfaces/prediction/predictionContext.tests.ts b/web/src/test/auto/headless/engine/interfaces/prediction/predictionContext.tests.ts index 15faf2189fd..709c2346ad6 100644 --- a/web/src/test/auto/headless/engine/interfaces/prediction/predictionContext.tests.ts +++ b/web/src/test/auto/headless/engine/interfaces/prediction/predictionContext.tests.ts @@ -5,7 +5,7 @@ import { LexicalModelTypes } from '@keymanapp/common-types'; import { LanguageProcessor, TranscriptionCache } from 'keyman/engine/main'; import { PredictionContext } from 'keyman/engine/interfaces'; -import { NodeWorker } from "@keymanapp/lexical-model-layer/node"; +import { nodePredictiveTextWorkerFactory } from "@keymanapp/lexical-model-layer/node"; import { SyntheticTextStore } from 'keyman/engine/keyboard'; import Suggestion = LexicalModelTypes.Suggestion; @@ -70,7 +70,7 @@ describe("PredictionContext", () => { let langProcessor: LanguageProcessor; beforeEach(function() { - langProcessor = new LanguageProcessor(NodeWorker, new TranscriptionCache()); + langProcessor = new LanguageProcessor(nodePredictiveTextWorkerFactory, '', new TranscriptionCache()); }); afterEach(function() { diff --git a/web/src/test/auto/headless/engine/main/inputProcessor.tests.ts b/web/src/test/auto/headless/engine/main/inputProcessor.tests.ts index ac9926467df..0bf7ccb5d28 100644 --- a/web/src/test/auto/headless/engine/main/inputProcessor.tests.ts +++ b/web/src/test/auto/headless/engine/main/inputProcessor.tests.ts @@ -7,7 +7,7 @@ import * as sinon from 'sinon'; import { LexicalModelTypes } from '@keymanapp/common-types'; import { KeyboardTest, RecordedPhysicalKeystroke, RecordedSequenceTestSet } from '@keymanapp/recorder-core'; -import { NodeWorker } from '@keymanapp/lexical-model-layer/node'; +import { nodePredictiveTextWorkerFactory } from '@keymanapp/lexical-model-layer/node'; import { DeviceSpec, KMWString } from 'keyman/common/web-utils'; import { InputProcessor } from 'keyman/engine/main'; @@ -58,7 +58,7 @@ KMWString.enableSupplementaryPlane(false); describe('InputProcessor', function() { describe('[[constructor]]', function () { it('should initialize without errors', function () { - let core = new InputProcessor(device, null, DEFAULT_PROCESSOR_INIT_OPTIONS); + let core = new InputProcessor(device, null, null, DEFAULT_PROCESSOR_INIT_OPTIONS); assert.isNotNull(core); }); @@ -68,7 +68,7 @@ describe('InputProcessor', function() { // Can construct without the second parameter; if so, the final assertion - .mayPredict // will be invalidated. (No worker, no ability to predict.) // @ts-ignore - core = new InputProcessor(device, NodeWorker, { + core = new InputProcessor(device, '', nodePredictiveTextWorkerFactory, { baseLayout: 'us', keyboardInterface: new JSKeyboardInterface({}, null, new VariableStoreTestSerializer()), defaultOutputRules: new DefaultOutputRules() @@ -103,7 +103,7 @@ describe('InputProcessor', function() { let testDistribution: KeyDistribution = []; let keyboardWithHarness: JSKeyboardInterface; - let mainWebScriptURL = require.resolve('@keymanapp/lm-worker/worker-main.js'); + let mainWebScriptURL = require.resolve('@keymanapp/lm-worker/worker-thread.js'); // Easy peasy long context: use the unminified main script for the predictive-text worker! let coreSourceCode = fs.readFileSync(mainWebScriptURL, 'utf-8'); @@ -140,7 +140,7 @@ describe('InputProcessor', function() { it('with minimal context (no fat-fingers)', function() { this.timeout(32); // ms - let core = new InputProcessor(device, null, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + let core = new InputProcessor(device, '', null, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); let context = new SyntheticTextStore("", 0); let keyboard = keyboardWithHarness.activeKeyboard; @@ -160,7 +160,7 @@ describe('InputProcessor', function() { // These often run on VMs, so we'll be a bit generous. // I mean, it IS long context, and time thresholding is disabled within Node. - let core = new InputProcessor(device, null, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + let core = new InputProcessor(device, '', null, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); let keyboard = keyboardWithHarness.activeKeyboard; let layout = keyboard.layout(DeviceSpec.FormFactor.Phone); @@ -179,7 +179,7 @@ describe('InputProcessor', function() { it('with minimal context (with fat-fingers)', function() { this.timeout(32); // ms - let core = new InputProcessor(device, null, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + let core = new InputProcessor(device, '', null, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); let context = new SyntheticTextStore("", 0); let keyboard = keyboardWithHarness.activeKeyboard; @@ -203,7 +203,7 @@ describe('InputProcessor', function() { // 'without fat-fingers' test. // It IS long context, and time thresholding is disabled within Node. - let core = new InputProcessor(device, null, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + let core = new InputProcessor(device, '', null, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); let keyboard = keyboardWithHarness.activeKeyboard; let layout = keyboard.layout(DeviceSpec.FormFactor.Phone); @@ -239,7 +239,7 @@ describe('InputProcessor', function() { for (let testSet of testsToRun.testSet) { it(testSet.msg ?? 'test', function() { this.timeout(32); // ms - let core = new InputProcessor(device, null, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + let core = new InputProcessor(device, '', null, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); let context = new SyntheticTextStore("", 0); let keyboard = keyboardWithHarness.activeKeyboard as JSKeyboard; @@ -309,7 +309,7 @@ describe('InputProcessor', function() { }); it('replaces appended whitespace when a manually-applied suggestion is followed by a K_SPACE (dummy models)', async () => { - const core = new InputProcessor(device, NodeWorker, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + const core = new InputProcessor(device, '', nodePredictiveTextWorkerFactory, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); const langProcessor = core.languageProcessor; try { @@ -366,7 +366,7 @@ describe('InputProcessor', function() { }); it('replaces appended whitespace when a manually-applied suggestion is followed by a K_SPACE (trie models)', async () => { - const core = new InputProcessor(device, NodeWorker, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + const core = new InputProcessor(device, '', nodePredictiveTextWorkerFactory, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); const langProcessor = core.languageProcessor; try { @@ -424,7 +424,7 @@ describe('InputProcessor', function() { }); it('auto-applies a suggestion properly when available and triggered appropriately', async () => { - const core = new InputProcessor(device, NodeWorker, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + const core = new InputProcessor(device, '', nodePredictiveTextWorkerFactory, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); const langProcessor = core.languageProcessor; try { @@ -476,7 +476,7 @@ describe('InputProcessor', function() { }); it('displays a reversion after manually applying a suggestion and immediately backspacing', async () => { - const core = new InputProcessor(device, NodeWorker, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + const core = new InputProcessor(device, '', nodePredictiveTextWorkerFactory, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); const langProcessor = core.languageProcessor; try { @@ -533,7 +533,7 @@ describe('InputProcessor', function() { }); it('displays a reversion after returning to the whitespace after a manually-applied suggestion', async () => { - const core = new InputProcessor(device, NodeWorker, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + const core = new InputProcessor(device, '', nodePredictiveTextWorkerFactory, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); const langProcessor = core.languageProcessor; try { @@ -594,7 +594,7 @@ describe('InputProcessor', function() { }); it("displays a reversion after returning to the end of a manually-applied suggestion's body", async () => { - const core = new InputProcessor(device, NodeWorker, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + const core = new InputProcessor(device, '', nodePredictiveTextWorkerFactory, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); const langProcessor = core.languageProcessor; try { @@ -656,7 +656,7 @@ describe('InputProcessor', function() { }); it("does not display a reversion after backspacing part of an applied suggestion", async () => { - const core = new InputProcessor(device, NodeWorker, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + const core = new InputProcessor(device, '', nodePredictiveTextWorkerFactory, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); const langProcessor = core.languageProcessor; try { @@ -720,7 +720,7 @@ describe('InputProcessor', function() { }); it('displays a reversion after auto-applying a suggestion and immediately backspacing', async () => { - const core = new InputProcessor(device, NodeWorker, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + const core = new InputProcessor(device, '', nodePredictiveTextWorkerFactory, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); const langProcessor = core.languageProcessor; try { @@ -775,7 +775,7 @@ describe('InputProcessor', function() { }); it('displays a reversion after returning to the whitespace after a auto-applied suggestion', async () => { - const core = new InputProcessor(device, NodeWorker, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + const core = new InputProcessor(device, '', nodePredictiveTextWorkerFactory, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); const langProcessor = core.languageProcessor; try { @@ -829,7 +829,7 @@ describe('InputProcessor', function() { }); it("displays a reversion after returning to the end of an auto-applied suggestion's body", async () => { - const core = new InputProcessor(device, NodeWorker, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + const core = new InputProcessor(device, '', nodePredictiveTextWorkerFactory, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); const langProcessor = core.languageProcessor; try { diff --git a/web/src/test/auto/headless/engine/main/languageProcessor.tests.ts b/web/src/test/auto/headless/engine/main/languageProcessor.tests.ts index 3c22f90f500..83360e8aebc 100644 --- a/web/src/test/auto/headless/engine/main/languageProcessor.tests.ts +++ b/web/src/test/auto/headless/engine/main/languageProcessor.tests.ts @@ -8,7 +8,7 @@ import path from 'node:path'; import { assert } from 'chai'; -import { NodeWorker as LMWorker } from "@keymanapp/lexical-model-layer/node"; +import { nodePredictiveTextWorkerFactory } from "@keymanapp/lexical-model-layer/node"; import { LexicalModelCompiler } from '@keymanapp/kmc-model'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { SyntheticTextStore } from 'keyman/engine/keyboard'; @@ -35,7 +35,7 @@ describe('LanguageProcessor', function() { const callbacks = new TestCompilerCallbacks(this); beforeEach(function() { - languageProcessor = new LanguageProcessor(LMWorker, new TranscriptionCache()); + languageProcessor = new LanguageProcessor(nodePredictiveTextWorkerFactory, '', new TranscriptionCache()); }); afterEach(function() { @@ -54,7 +54,7 @@ describe('LanguageProcessor', function() { constructInstance() { throw new Error("Simulating a platform without Worker support"); } - }, new TranscriptionCache()); + }, '', new TranscriptionCache()); assert.isFalse(workerlessProcessor.canEnable); } catch { assert.fail('Worker construction error was unhandled'); diff --git a/web/src/test/auto/integrated/test_utils.ts b/web/src/test/auto/integrated/test_utils.ts index 585fac80849..1703388d76b 100644 --- a/web/src/test/auto/integrated/test_utils.ts +++ b/web/src/test/auto/integrated/test_utils.ts @@ -13,7 +13,7 @@ export let DEVICE_DETECT_FAILURE = false; const loc = document.location; // config.testFile generally starts with a '/', with the path resembling the actual full local // filesystem for the drive. -const domain = `${loc.protocol}/${loc.host}` +const domain = `${loc.protocol}//${loc.host}`; // If we've set things up to support Device dection without loading KMW... try { From 7e04aa7bee85d7c9bc33f34f855289567f5ef737 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 17 Sep 2026 07:41:04 +0200 Subject: [PATCH 3/5] refactor: add worker-thread to iOS and Android builds --- android/.gitignore | 2 ++ .../app/src/main/java/com/keyman/engine/KMManager.java | 3 +++ android/KMEA/build.sh | 2 ++ common/web/sentry-manager/src/index.ts | 1 + ios/.gitignore | 2 ++ .../Classes/Resource Management/Storage.swift | 5 +++++ ios/engine/build.sh | 1 + web/src/app/webview/build.sh | 8 ++++++++ 8 files changed, 24 insertions(+) diff --git a/android/.gitignore b/android/.gitignore index e822a25c2b0..37e28d25905 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -37,6 +37,8 @@ KMEA/**/assets/keymanandroid.js KMEA/**/assets/keyman.js.map KMEA/**/assets/keymanweb-webview.js KMEA/**/assets/keymanweb-webview.js.map +KMEA/**/assets/worker-thread.js +KMEA/**/assets/worker-thread.js.map KMEA/**/assets/map-polyfill.js KMEA/**/assets/sentry.min.js KMEA/**/assets/keyman-sentry.js diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java index 80701ad6835..bdb6f6ad784 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java @@ -375,6 +375,7 @@ public int toInt() { // Keyman files protected static final String KMFilename_KeyboardHtml = "keyboard.html"; protected static final String KMFilename_JSEngine = "keymanweb-webview.js"; + protected static final String KMFilename_LMWorker = "worker-thread.js"; protected static final String KMFilename_JSSentry = "keyman-sentry.js"; protected static final String KMFilename_AndroidHost = "android-host.js"; protected static final String KMFilename_KmwCss = "kmwosk.css"; @@ -1075,10 +1076,12 @@ private static void copyAssets(Context context) { AssetManager assetManager = context.getAssets(); try { + // TODO: these files should be in a constant array, not enumerated separately here (see also iOS) // Copy KMW files copyAsset(context, KMFilename_KeyboardHtml, "", true); copyAsset(context, KMFilename_JSEngine, "", true); + copyAsset(context, KMFilename_LMWorker, "", true); copyAsset(context, KMFilename_JSSentry, "", true); copyAsset(context, KMFilename_AndroidHost, "", true); copyAsset(context, KMFilename_KmwCss, "", true); diff --git a/android/KMEA/build.sh b/android/KMEA/build.sh index 66471c0bbb3..5cd0ce87b7a 100755 --- a/android/KMEA/build.sh +++ b/android/KMEA/build.sh @@ -55,6 +55,8 @@ do_build() { builder_echo "Copying Keyman Web artifacts" cp "$KEYMAN_ROOT/web/build/app/webview/$BUILDER_CONFIGURATION/keymanweb-webview.js" "$ENGINE_ASSETS/" cp "$KEYMAN_ROOT/web/build/app/webview/$BUILDER_CONFIGURATION/keymanweb-webview.js.map" "$ENGINE_ASSETS/" + cp "$KEYMAN_ROOT/web/build/app/webview/$BUILDER_CONFIGURATION/worker-thread.js" "$ENGINE_ASSETS/" + cp "$KEYMAN_ROOT/web/build/app/webview/$BUILDER_CONFIGURATION/worker-thread.js.map" "$ENGINE_ASSETS/" cp "$KEYMAN_ROOT/web/build/app/webview/$BUILDER_CONFIGURATION/map-polyfill.js" "$ENGINE_ASSETS/" cp "$KEYMAN_ROOT/web/build/app/resources/osk/ajax-loader.gif" "$ENGINE_ASSETS/" cp "$KEYMAN_ROOT/web/build/app/resources/osk/kmwosk.css" "$ENGINE_ASSETS/" diff --git a/common/web/sentry-manager/src/index.ts b/common/web/sentry-manager/src/index.ts index a87694e4e0e..f8e7a39eec1 100644 --- a/common/web/sentry-manager/src/index.ts +++ b/common/web/sentry-manager/src/index.ts @@ -18,6 +18,7 @@ export class KeymanSentryManager { static STANDARD_ALIASABLE_FILES = { 'keymanweb.js': 'keymanweb.js', 'keymanweb-webview.js': 'keymanweb-webview.js', + 'worker-thread.js': 'worker-thread.js', 'kmwuibutton.js': 'kmwuibutton.js', 'kmwuifloat.js': 'kmwuifloat.js', 'kmwuitoggle.js': 'kmwuitoggle.js', diff --git a/ios/.gitignore b/ios/.gitignore index 556562d4b6e..8c13749b0d7 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -18,6 +18,8 @@ engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/kmwosk.css engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keyman.js.map engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keymanweb-webview.js engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keymanweb-webview.js.map +engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/worker-thread.js +engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/worker-thread.js.map engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keyman-sentry.js engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/sentry.min.js Keyman 2* diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/Storage.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/Storage.swift index fd8b98e8a88..8ac69704cc2 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/Storage.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/Storage.swift @@ -225,6 +225,7 @@ class Storage { // MARK: - Copying extension Storage { func copyKMWFiles(from bundle: Bundle) throws { + // TODO: these should be in a constant array, not enumerated individually here (see also Android) try Storage.copy(from: bundle, resourceName: Resources.kmwFilename, dstDir: baseDir, @@ -237,6 +238,10 @@ extension Storage { resourceName: "keymanweb-webview.js", dstDir: baseDir, excludeFromBackup: true) + try Storage.copy(from: bundle, + resourceName: "worker-thread.js", + dstDir: baseDir, + excludeFromBackup: true) try Storage.copy(from: bundle, resourceName: "keyman-sentry.js", dstDir: baseDir, diff --git a/ios/engine/build.sh b/ios/engine/build.sh index 2fa7add0828..fe3e38b216c 100755 --- a/ios/engine/build.sh +++ b/ios/engine/build.sh @@ -109,6 +109,7 @@ function update_bundle ( ) { cp "$KMW_RESOURCES/osk/kmwosk.css" "$BUNDLE_PATH/kmwosk.css" cp "$KMW_RESOURCES/osk/keymanweb-osk.ttf" "$BUNDLE_PATH/keymanweb-osk.ttf" cp "$KMW_PRODUCT/keymanweb-webview.js" "$BUNDLE_PATH/keymanweb-webview.js" + cp "$KMW_PRODUCT/worker-thread.js" "$BUNDLE_PATH/worker-thread.js" cp "$KEYMAN_ROOT/common/web/sentry-manager/build/lib/index.js" "$BUNDLE_PATH/keyman-sentry.js" } diff --git a/web/src/app/webview/build.sh b/web/src/app/webview/build.sh index 8e150d0796c..f377b4ac74a 100755 --- a/web/src/app/webview/build.sh +++ b/web/src/app/webview/build.sh @@ -85,6 +85,14 @@ compile_and_copy() { node map-polyfill-bundler.js + cp "${KEYMAN_ROOT}/web/src/engine/predictive-text/worker-thread/build/lib/worker-thread.js" "${BUILD_ROOT}/debug/worker-thread.js" + cp "${KEYMAN_ROOT}/web/src/engine/predictive-text/worker-thread/build/lib/worker-thread.js.map" "${BUILD_ROOT}/debug/worker-thread.js.map" + cp "${KEYMAN_ROOT}/web/src/engine/predictive-text/worker-thread/build/lib/worker-thread.d.ts" "${BUILD_ROOT}/debug/worker-thread.d.ts" + + # TODO: rename to lm-worker.js? do we keep debug/release distinction through .js vs .min.js or via folder? + cp "${KEYMAN_ROOT}/web/src/engine/predictive-text/worker-thread/build/lib/worker-thread.min.js" "${BUILD_ROOT}/release/worker-thread.js" + cp "${KEYMAN_ROOT}/web/src/engine/predictive-text/worker-thread/build/lib/worker-thread.min.js.map" "${BUILD_ROOT}/release/worker-thread.js.map" + # For dependent test pages. builder_launch /web/src/test/manual/embed/android-harness/build.sh configure,build } From 13ac0953e723695f0a658671955519db1ba1d8ba Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Wed, 23 Sep 2026 12:56:33 +0200 Subject: [PATCH 4/5] refactor(web): cleanup worker instantiation Co-authored-by: Eberhard Beilharz Co-authored-by: Joshua Prins-Horton --- web/src/app/browser/src/main.ts | 4 +-- web/src/app/webview/src/main.ts | 4 +-- .../predictive-text/worker-main/src/index.ts | 4 +-- .../worker-main/src/node/index.ts | 2 +- .../node-predictive-text-worker-factory.ts | 22 ++++++++++++ .../src/node/node-predictive-text-worker.ts | 23 ++++++------ .../src/node/node-worker-factory.ts | 15 -------- .../worker-main/src/web/index.ts | 2 +- .../web/web-predictive-text-worker-factory.ts | 12 +++++++ .../worker-main/src/web/web-worker-factory.ts | 23 ------------ .../worker-main/src/worker-factory.ts | 2 +- .../worker-dummy-integration.tests.js | 4 +-- .../headless/worker-trie-integration.tests.js | 4 +-- .../cases/top-level-lmlayer.tests.ts | 6 ++-- .../cases/worker-dummy-integration.tests.ts | 8 ++--- .../cases/worker-trie-integration.tests.ts | 8 ++--- .../unit_tests/in_browser/helpers.mjs | 8 ++++- .../src/main/headless/inputProcessor.ts | 4 +-- .../src/main/headless/languageProcessor.ts | 4 +-- web/src/engine/src/main/keymanEngineBase.ts | 2 +- .../prediction/predictionContext.tests.ts | 5 +-- .../engine/main/inputProcessor.tests.ts | 36 +++++++++---------- .../engine/main/languageProcessor.tests.ts | 7 ++-- web/src/test/auto/resources/index.ts | 6 +++- 24 files changed, 113 insertions(+), 102 deletions(-) create mode 100644 web/src/engine/predictive-text/worker-main/src/node/node-predictive-text-worker-factory.ts delete mode 100644 web/src/engine/predictive-text/worker-main/src/node/node-worker-factory.ts create mode 100644 web/src/engine/predictive-text/worker-main/src/web/web-predictive-text-worker-factory.ts delete mode 100644 web/src/engine/predictive-text/worker-main/src/web/web-worker-factory.ts diff --git a/web/src/app/browser/src/main.ts b/web/src/app/browser/src/main.ts index 65441509667..99b57f491af 100644 --- a/web/src/app/browser/src/main.ts +++ b/web/src/app/browser/src/main.ts @@ -3,7 +3,7 @@ */ import { KeymanEngine } from './keymanEngine.js' -import { webPredictiveTextWorkerFactory } from '@keymanapp/lexical-model-layer/web' +import { WebPredictiveTextWorkerFactory } from '@keymanapp/lexical-model-layer/web' /** * Determine path and protocol of executing script, setting them as @@ -13,4 +13,4 @@ const ss = (document.currentScript as HTMLScriptElement)?.src; const sPath = ss ? ss.substring(0, ss.lastIndexOf('/') + 1) : './'; // @ts-ignore -window['keyman'] = new KeymanEngine(webPredictiveTextWorkerFactory, sPath); \ No newline at end of file +window['keyman'] = new KeymanEngine(new WebPredictiveTextWorkerFactory(sPath + 'worker-thread.js'), sPath); \ No newline at end of file diff --git a/web/src/app/webview/src/main.ts b/web/src/app/webview/src/main.ts index 65441509667..99b57f491af 100644 --- a/web/src/app/webview/src/main.ts +++ b/web/src/app/webview/src/main.ts @@ -3,7 +3,7 @@ */ import { KeymanEngine } from './keymanEngine.js' -import { webPredictiveTextWorkerFactory } from '@keymanapp/lexical-model-layer/web' +import { WebPredictiveTextWorkerFactory } from '@keymanapp/lexical-model-layer/web' /** * Determine path and protocol of executing script, setting them as @@ -13,4 +13,4 @@ const ss = (document.currentScript as HTMLScriptElement)?.src; const sPath = ss ? ss.substring(0, ss.lastIndexOf('/') + 1) : './'; // @ts-ignore -window['keyman'] = new KeymanEngine(webPredictiveTextWorkerFactory, sPath); \ No newline at end of file +window['keyman'] = new KeymanEngine(new WebPredictiveTextWorkerFactory(sPath + 'worker-thread.js'), sPath); \ No newline at end of file diff --git a/web/src/engine/predictive-text/worker-main/src/index.ts b/web/src/engine/predictive-text/worker-main/src/index.ts index 98685e8c494..39eb7f39430 100644 --- a/web/src/engine/predictive-text/worker-main/src/index.ts +++ b/web/src/engine/predictive-text/worker-main/src/index.ts @@ -4,5 +4,5 @@ export { LMLayer } from './lmlayer.js'; export { type WorkerFactory } from './worker-factory.js'; -export { nodePredictiveTextWorkerFactory, NodePredictiveTextWorkerFactory } from './node/node-worker-factory.js'; -export { webPredictiveTextWorkerFactory, WebPredictiveTextWorkerFactory } from './web/web-worker-factory.js'; +export { NodePredictiveTextWorkerFactory } from './node/node-predictive-text-worker-factory.js'; +export { WebPredictiveTextWorkerFactory } from './web/web-predictive-text-worker-factory.js'; diff --git a/web/src/engine/predictive-text/worker-main/src/node/index.ts b/web/src/engine/predictive-text/worker-main/src/node/index.ts index db8afac52be..ede69d81a76 100644 --- a/web/src/engine/predictive-text/worker-main/src/node/index.ts +++ b/web/src/engine/predictive-text/worker-main/src/node/index.ts @@ -4,4 +4,4 @@ export { LMLayer } from '../lmlayer.js'; export { type WorkerFactory } from '../worker-factory.js'; -export { nodePredictiveTextWorkerFactory, NodePredictiveTextWorkerFactory } from './node-worker-factory.js'; +export { NodePredictiveTextWorkerFactory } from './node-predictive-text-worker-factory.js'; diff --git a/web/src/engine/predictive-text/worker-main/src/node/node-predictive-text-worker-factory.ts b/web/src/engine/predictive-text/worker-main/src/node/node-predictive-text-worker-factory.ts new file mode 100644 index 00000000000..4e92e450caf --- /dev/null +++ b/web/src/engine/predictive-text/worker-main/src/node/node-predictive-text-worker-factory.ts @@ -0,0 +1,22 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ +import { WorkerFactory } from "../worker-factory.js"; +import { NodePredictiveTextWorker } from "./node-predictive-text-worker.js"; + +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename__ = fileURLToPath(import.meta.url); +const __dirname__ = path.dirname(__filename__); +export const nodeLexicalModelWorkerPath = __dirname__ + "/../../../../worker-thread/build/lib/worker-thread.js"; + +export class NodePredictiveTextWorkerFactory implements WorkerFactory { + private workerFilename: string; + constructor(workerFilename?: string) { + this.workerFilename = workerFilename ?? nodeLexicalModelWorkerPath; + } + constructInstance(): Worker { + return new NodePredictiveTextWorker(this.workerFilename); + } +} diff --git a/web/src/engine/predictive-text/worker-main/src/node/node-predictive-text-worker.ts b/web/src/engine/predictive-text/worker-main/src/node/node-predictive-text-worker.ts index b450ebd7d23..ef1928a4b2c 100644 --- a/web/src/engine/predictive-text/worker-main/src/node/node-predictive-text-worker.ts +++ b/web/src/engine/predictive-text/worker-main/src/node/node-predictive-text-worker.ts @@ -1,23 +1,26 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * NodePredictiveTextWorker mirrors the necessary interfaces for + * WebPredictiveTextWorker in Node. It is used only for unit tests. + */ + // We use a subset of the Worker interface here; compiling directly against the true // WebWorker type definitions would require us to implement more methods than we do. /// // Defines types related to Node workers. import * as worker from 'node:worker_threads'; -import * as path from 'node:path'; import { Buffer } from 'node:buffer'; -import { fileURLToPath, URL } from 'node:url'; +import { URL } from 'node:url'; -const __filename__ = fileURLToPath(import.meta.url); -const __dirname__ = path.dirname(__filename__); -const workerThreadFilename = __dirname__ + "/../../../../worker-thread/build/lib/worker-thread.js"; /** * Defines mappings from Node Worker signatures to WebWorker signatures * * TODO: move this to a separate module, no need for it to be embedded string */ -const nodeWorkerToWebWorkerMappingSource = ` +const nodeWorkerToWebWorkerMappingSource = (filename: string) => (` import { parentPort } from 'node:worker_threads'; import fs from 'node:fs'; import vm from 'node:vm'; @@ -51,8 +54,8 @@ self.postMessage = postMessage; self.importScripts = importScripts; self.self = self; // make it global! // Start off by importing the main worker itself -importScripts(${JSON.stringify(workerThreadFilename)}); -`; +importScripts(${JSON.stringify(filename)}); +`); /** * Uses the Node version of Workers to provide proper, authentic separate-thread @@ -60,8 +63,8 @@ importScripts(${JSON.stringify(workerThreadFilename)}); * necessary to run the WebWorker-oriented worker code. */ export class NodePredictiveTextWorker extends worker.Worker implements Worker { - constructor() { - const buffer = Buffer.from(nodeWorkerToWebWorkerMappingSource); + constructor(workerFilename: string) { + const buffer = Buffer.from(nodeWorkerToWebWorkerMappingSource(workerFilename)); const dataSrc = "data:text/javascript;base64," + buffer.toString('base64'); //@ts-ignore super(new URL(dataSrc)); diff --git a/web/src/engine/predictive-text/worker-main/src/node/node-worker-factory.ts b/web/src/engine/predictive-text/worker-main/src/node/node-worker-factory.ts deleted file mode 100644 index e0f1821d939..00000000000 --- a/web/src/engine/predictive-text/worker-main/src/node/node-worker-factory.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Keyman is copyright (C) SIL Global. MIT License. - */ -import { WorkerFactory } from "../worker-factory.js"; -import { NodePredictiveTextWorker } from "./node-predictive-text-worker.js"; - -export class NodePredictiveTextWorkerFactory implements WorkerFactory { - constructInstance(_rootPath?: string): Worker { - // note: rootPath is never used for Node worker, as it is only instanatiated - // in unit tests, where paths are known - return new NodePredictiveTextWorker(); - } -} - -export const nodePredictiveTextWorkerFactory = new NodePredictiveTextWorkerFactory(); diff --git a/web/src/engine/predictive-text/worker-main/src/web/index.ts b/web/src/engine/predictive-text/worker-main/src/web/index.ts index 76d84fee0a6..36fe8aae5af 100644 --- a/web/src/engine/predictive-text/worker-main/src/web/index.ts +++ b/web/src/engine/predictive-text/worker-main/src/web/index.ts @@ -4,4 +4,4 @@ export { LMLayer } from '../lmlayer.js'; export { type WorkerFactory } from '../worker-factory.js'; -export { webPredictiveTextWorkerFactory, WebPredictiveTextWorkerFactory } from './web-worker-factory.js'; +export { WebPredictiveTextWorkerFactory } from './web-predictive-text-worker-factory.js'; diff --git a/web/src/engine/predictive-text/worker-main/src/web/web-predictive-text-worker-factory.ts b/web/src/engine/predictive-text/worker-main/src/web/web-predictive-text-worker-factory.ts new file mode 100644 index 00000000000..1b6c5f47244 --- /dev/null +++ b/web/src/engine/predictive-text/worker-main/src/web/web-predictive-text-worker-factory.ts @@ -0,0 +1,12 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +import { WorkerFactory } from "../worker-factory.js"; + +export class WebPredictiveTextWorkerFactory implements WorkerFactory { + constructor(private workerFilename: string) {} + constructInstance(): Worker { + return new Worker(this.workerFilename); + } +} diff --git a/web/src/engine/predictive-text/worker-main/src/web/web-worker-factory.ts b/web/src/engine/predictive-text/worker-main/src/web/web-worker-factory.ts deleted file mode 100644 index f1aeedcca99..00000000000 --- a/web/src/engine/predictive-text/worker-main/src/web/web-worker-factory.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Keyman is copyright (C) SIL Global. MIT License. - */ - -import { WorkerFactory } from "../worker-factory.js"; - -export class WebPredictiveTextWorkerFactory implements WorkerFactory { - constructInstance(rootPath?: string): Worker { - if(!rootPath) { - rootPath = './'; - } - else if(!rootPath.endsWith('/')) { - rootPath = rootPath + '/'; - } - return new Worker(rootPath + 'worker-thread.js'); - } - - constructTestInstance(): Worker { - return new Worker('/web/src/engine/predictive-text/worker-thread/build/lib/worker-thread.js'); - } -} - -export const webPredictiveTextWorkerFactory = new WebPredictiveTextWorkerFactory(); diff --git a/web/src/engine/predictive-text/worker-main/src/worker-factory.ts b/web/src/engine/predictive-text/worker-main/src/worker-factory.ts index 7f40f2a4669..76b79f030d5 100644 --- a/web/src/engine/predictive-text/worker-main/src/worker-factory.ts +++ b/web/src/engine/predictive-text/worker-main/src/worker-factory.ts @@ -3,5 +3,5 @@ */ export interface WorkerFactory { - constructInstance(rootPath?: string): Worker; + constructInstance(): Worker; }; \ No newline at end of file diff --git a/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-dummy-integration.tests.js b/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-dummy-integration.tests.js index b5477e22489..b2f79caeab1 100644 --- a/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-dummy-integration.tests.js +++ b/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-dummy-integration.tests.js @@ -4,7 +4,7 @@ import fs from 'fs'; import { createRequire } from 'module'; const require = createRequire(import.meta.url); -import { LMLayer, nodePredictiveTextWorkerFactory } from '#./node/index.js'; +import { LMLayer, NodePredictiveTextWorkerFactory } from '#./node/index.js'; import { capabilities, iGotDistractedByHazel } from '@keymanapp/common-test-resources/model-helpers.mjs'; /* @@ -20,7 +20,7 @@ describe('LMLayer using dummy model', function () { let worker; beforeEach(function() { - worker = nodePredictiveTextWorkerFactory.constructInstance(); + worker = (new NodePredictiveTextWorkerFactory()).constructInstance(); lmLayer = new LMLayer(capabilities(), worker); }); diff --git a/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-trie-integration.tests.js b/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-trie-integration.tests.js index 05716f5c7ab..e9cf967f205 100644 --- a/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-trie-integration.tests.js +++ b/web/src/engine/predictive-text/worker-main/unit_tests/headless/worker-trie-integration.tests.js @@ -3,7 +3,7 @@ import { assert } from 'chai'; import { createRequire } from 'module'; const require = createRequire(import.meta.url); -import { LMLayer, nodePredictiveTextWorkerFactory } from '#./node/index.js'; +import { LMLayer, NodePredictiveTextWorkerFactory } from '#./node/index.js'; import { capabilities } from '@keymanapp/common-test-resources/model-helpers.mjs'; /* @@ -14,7 +14,7 @@ describe('LMLayer using the trie model', function () { let worker; beforeEach(function() { - worker = nodePredictiveTextWorkerFactory.constructInstance(); + worker = (new NodePredictiveTextWorkerFactory()).constructInstance(); lmLayer = new LMLayer(capabilities(), worker, true); }); diff --git a/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/top-level-lmlayer.tests.ts b/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/top-level-lmlayer.tests.ts index cab08232c81..ee7de431fa8 100644 --- a/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/top-level-lmlayer.tests.ts +++ b/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/top-level-lmlayer.tests.ts @@ -1,16 +1,16 @@ import { assert } from 'chai'; -import { LMLayer, webPredictiveTextWorkerFactory } from "@keymanapp/lexical-model-layer/web"; +import { LMLayer, WebPredictiveTextWorkerFactory } from "@keymanapp/lexical-model-layer/web"; import { DEFAULT_BROWSER_TIMEOUT } from '@keymanapp/common-test-resources/test-timeouts.mjs'; -import { defaultCapabilities } from '../helpers.mjs'; +import { defaultCapabilities, predictiveTextWorkerFilename } from '../helpers.mjs'; describe('LMLayer', function () { this.timeout(DEFAULT_BROWSER_TIMEOUT); describe('[[constructor]]', function () { it('should construct with a single argument', function () { - let lmLayer = new LMLayer(defaultCapabilities, webPredictiveTextWorkerFactory.constructTestInstance(), true); + let lmLayer = new LMLayer(defaultCapabilities, (new WebPredictiveTextWorkerFactory(predictiveTextWorkerFilename)).constructInstance(), true); assert.instanceOf(lmLayer, LMLayer); lmLayer.shutdown(); }); diff --git a/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/worker-dummy-integration.tests.ts b/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/worker-dummy-integration.tests.ts index 4ce3d0489af..b1ed0126a4e 100644 --- a/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/worker-dummy-integration.tests.ts +++ b/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/worker-dummy-integration.tests.ts @@ -1,9 +1,9 @@ import { assert } from 'chai'; -import { LMLayer, webPredictiveTextWorkerFactory } from "@keymanapp/lexical-model-layer/web"; +import { LMLayer, WebPredictiveTextWorkerFactory } from "@keymanapp/lexical-model-layer/web"; import { DEFAULT_BROWSER_TIMEOUT } from '@keymanapp/common-test-resources/test-timeouts.mjs'; -import { defaultCapabilities } from '../helpers.mjs'; +import { defaultCapabilities, predictiveTextWorkerFilename } from '../helpers.mjs'; // Import assertions, even using 'with', aren't yet supported in Firefox's engine. // import hazelModel from '@keymanapp/common-test-resources/json/models/future_suggestions/i_got_distracted_by_hazel.json' with { type: 'json' }; @@ -52,7 +52,7 @@ describe('LMLayer using dummy model', function () { describe('Prediction', function () { it('will predict future suggestions', function () { - var lmLayer = new LMLayer(defaultCapabilities, webPredictiveTextWorkerFactory.constructTestInstance(), true); + var lmLayer = new LMLayer(defaultCapabilities, (new WebPredictiveTextWorkerFactory(predictiveTextWorkerFilename)).constructInstance(), true); var stripIDs = function(suggestions) { suggestions.forEach(function(suggestion) { @@ -93,7 +93,7 @@ describe('LMLayer using dummy model', function () { describe('Wordbreaking', function () { it('will perform (default) wordbreaking and return word at caret', function () { - var lmLayer = new LMLayer(defaultCapabilities, webPredictiveTextWorkerFactory.constructTestInstance()); + var lmLayer = new LMLayer(defaultCapabilities, (new WebPredictiveTextWorkerFactory(predictiveTextWorkerFilename)).constructInstance()); // We're testing many as asynchronous messages in a row. // this would be cleaner using async/await syntax, but diff --git a/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/worker-trie-integration.tests.ts b/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/worker-trie-integration.tests.ts index 8f266e20260..32367f7ba1e 100644 --- a/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/worker-trie-integration.tests.ts +++ b/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/cases/worker-trie-integration.tests.ts @@ -1,8 +1,8 @@ import { assert } from 'chai'; -import { LMLayer, webPredictiveTextWorkerFactory } from "@keymanapp/lexical-model-layer/web"; +import { LMLayer, WebPredictiveTextWorkerFactory } from "@keymanapp/lexical-model-layer/web"; import { DEFAULT_BROWSER_TIMEOUT } from '@keymanapp/common-test-resources/test-timeouts.mjs'; -import { defaultCapabilities } from '../helpers.mjs'; +import { defaultCapabilities, predictiveTextWorkerFilename } from '../helpers.mjs'; // Import assertions, even using 'with', aren't yet supported in Firefox's engine. // import hazelModel from '@keymanapp/common-test-resources/json/models/future_suggestions/i_got_distracted_by_hazel.json' with { type: 'json' }; @@ -29,7 +29,7 @@ describe('LMLayer using the trie model', function () { // Parameter 3 = true: enables 'test mode', disables correction-search timeout. // This helps prevent the correction-search timeout from flaking out periodically during unit tests in // CI, since remote servers / devices are involved. - var lmLayer = new LMLayer(defaultCapabilities, webPredictiveTextWorkerFactory.constructTestInstance(), true); + var lmLayer = new LMLayer(defaultCapabilities, (new WebPredictiveTextWorkerFactory(predictiveTextWorkerFilename)).constructInstance(), true); // We're testing many as asynchronous messages in a row. // this would be cleaner using async/await syntax, but @@ -71,7 +71,7 @@ describe('LMLayer using the trie model', function () { // // https://community.software.sil.org/t/search-term-to-key-in-lexical-model-not-working-both-ways-by-default/3133 it('should use the default searchTermToKey()', function () { - var lmLayer = new LMLayer(defaultCapabilities, webPredictiveTextWorkerFactory.constructTestInstance(), /* testMode */ true); + var lmLayer = new LMLayer(defaultCapabilities, (new WebPredictiveTextWorkerFactory(predictiveTextWorkerFilename)).constructInstance(), /* testMode */ true); let loc = document.location; return lmLayer.loadModel( diff --git a/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/helpers.mjs b/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/helpers.mjs index 385379bde64..fc4a0fc2ce7 100644 --- a/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/helpers.mjs +++ b/web/src/engine/predictive-text/worker-main/unit_tests/in_browser/helpers.mjs @@ -1,3 +1,9 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + export let defaultCapabilities = { maxLeftContextCodeUnits: 64 -}; \ No newline at end of file +}; + +export const predictiveTextWorkerFilename = 'web/build/publish/debug/worker-thread.js'; \ No newline at end of file diff --git a/web/src/engine/src/main/headless/inputProcessor.ts b/web/src/engine/src/main/headless/inputProcessor.ts index f21d76964ad..f82807284ac 100644 --- a/web/src/engine/src/main/headless/inputProcessor.ts +++ b/web/src/engine/src/main/headless/inputProcessor.ts @@ -53,7 +53,7 @@ export class InputProcessor { private readonly contextCache = new TranscriptionCache(); - constructor(device: DeviceSpec, sourcePath: string, predictiveWorkerFactory: WorkerFactory, options: ProcessorInitOptions) { + constructor(device: DeviceSpec, predictiveWorkerFactory: WorkerFactory, options: ProcessorInitOptions) { if(!device) { throw new Error('device must be defined'); } @@ -61,7 +61,7 @@ export class InputProcessor { this.contextDevice = device; this.jsKbdProcessor = new JSKeyboardProcessor(device, options); this._keyboardProcessor = this.jsKbdProcessor; - this._languageProcessor = new LanguageProcessor(predictiveWorkerFactory, sourcePath, this.contextCache); + this._languageProcessor = new LanguageProcessor(predictiveWorkerFactory, this.contextCache); } public async init(paths: PathConfiguration, storeSerializer: VariableStoreSerializer): Promise { diff --git a/web/src/engine/src/main/headless/languageProcessor.ts b/web/src/engine/src/main/headless/languageProcessor.ts index 960a8ad1e2e..0107d364f99 100644 --- a/web/src/engine/src/main/headless/languageProcessor.ts +++ b/web/src/engine/src/main/headless/languageProcessor.ts @@ -25,7 +25,7 @@ export class LanguageProcessor extends EventEmitter { private _state: StateChangeEnum = 'inactive'; - public constructor(predictiveWorkerFactory: WorkerFactory, sourcePath: string, transcriptionCache: TranscriptionCache, supportsRightDeletions: boolean = false) { + public constructor(predictiveWorkerFactory: WorkerFactory, transcriptionCache: TranscriptionCache, supportsRightDeletions: boolean = false) { super(); this.recentTranscriptions = transcriptionCache; @@ -44,7 +44,7 @@ export class LanguageProcessor extends EventEmitter { let workerInstance: Worker; try { - workerInstance = predictiveWorkerFactory?.constructInstance(sourcePath); + workerInstance = predictiveWorkerFactory?.constructInstance(); } catch(e) { // We can condition on `lmEngine` being null/undefined. console.warn('Web workers are not available: ' + (e ?? '').toString()); diff --git a/web/src/engine/src/main/keymanEngineBase.ts b/web/src/engine/src/main/keymanEngineBase.ts index aa74ca85c41..1318818c288 100644 --- a/web/src/engine/src/main/keymanEngineBase.ts +++ b/web/src/engine/src/main/keymanEngineBase.ts @@ -126,7 +126,7 @@ export class KeymanEngineBase< const processorConfiguration = processorConfigInitializer(this); processorConfiguration.baseLayout = determineBaseLayout(); this.interface = processorConfiguration.keyboardInterface as KeyboardInterfaceBase; - this.core = new InputProcessor(config.hostDevice, config.sourcePath, workerFactory, processorConfiguration); + this.core = new InputProcessor(config.hostDevice, workerFactory, processorConfiguration); this.core.languageProcessor.on('statechange', (state) => { // The banner controller cannot directly trigger a layout-refresh at this time, diff --git a/web/src/test/auto/headless/engine/interfaces/prediction/predictionContext.tests.ts b/web/src/test/auto/headless/engine/interfaces/prediction/predictionContext.tests.ts index 709c2346ad6..bd8ab1116d7 100644 --- a/web/src/test/auto/headless/engine/interfaces/prediction/predictionContext.tests.ts +++ b/web/src/test/auto/headless/engine/interfaces/prediction/predictionContext.tests.ts @@ -5,10 +5,11 @@ import { LexicalModelTypes } from '@keymanapp/common-types'; import { LanguageProcessor, TranscriptionCache } from 'keyman/engine/main'; import { PredictionContext } from 'keyman/engine/interfaces'; -import { nodePredictiveTextWorkerFactory } from "@keymanapp/lexical-model-layer/node"; +import { NodePredictiveTextWorkerFactory } from "@keymanapp/lexical-model-layer/node"; import { SyntheticTextStore } from 'keyman/engine/keyboard'; import Suggestion = LexicalModelTypes.Suggestion; +import { getPredictiveTextWorkerPath } from 'keyman/test/resources'; function compileDummyModel(suggestionSets: Suggestion[][]) { return ` @@ -70,7 +71,7 @@ describe("PredictionContext", () => { let langProcessor: LanguageProcessor; beforeEach(function() { - langProcessor = new LanguageProcessor(nodePredictiveTextWorkerFactory, '', new TranscriptionCache()); + langProcessor = new LanguageProcessor(new NodePredictiveTextWorkerFactory(getPredictiveTextWorkerPath()), new TranscriptionCache()); }); afterEach(function() { diff --git a/web/src/test/auto/headless/engine/main/inputProcessor.tests.ts b/web/src/test/auto/headless/engine/main/inputProcessor.tests.ts index 0bf7ccb5d28..9ae40b8b200 100644 --- a/web/src/test/auto/headless/engine/main/inputProcessor.tests.ts +++ b/web/src/test/auto/headless/engine/main/inputProcessor.tests.ts @@ -7,7 +7,7 @@ import * as sinon from 'sinon'; import { LexicalModelTypes } from '@keymanapp/common-types'; import { KeyboardTest, RecordedPhysicalKeystroke, RecordedSequenceTestSet } from '@keymanapp/recorder-core'; -import { nodePredictiveTextWorkerFactory } from '@keymanapp/lexical-model-layer/node'; +import { NodePredictiveTextWorkerFactory } from '@keymanapp/lexical-model-layer/node'; import { DeviceSpec, KMWString } from 'keyman/common/web-utils'; import { InputProcessor } from 'keyman/engine/main'; @@ -58,7 +58,7 @@ KMWString.enableSupplementaryPlane(false); describe('InputProcessor', function() { describe('[[constructor]]', function () { it('should initialize without errors', function () { - let core = new InputProcessor(device, null, null, DEFAULT_PROCESSOR_INIT_OPTIONS); + let core = new InputProcessor(device, null, DEFAULT_PROCESSOR_INIT_OPTIONS); assert.isNotNull(core); }); @@ -68,7 +68,7 @@ describe('InputProcessor', function() { // Can construct without the second parameter; if so, the final assertion - .mayPredict // will be invalidated. (No worker, no ability to predict.) // @ts-ignore - core = new InputProcessor(device, '', nodePredictiveTextWorkerFactory, { + core = new InputProcessor(device, new NodePredictiveTextWorkerFactory(), { baseLayout: 'us', keyboardInterface: new JSKeyboardInterface({}, null, new VariableStoreTestSerializer()), defaultOutputRules: new DefaultOutputRules() @@ -140,7 +140,7 @@ describe('InputProcessor', function() { it('with minimal context (no fat-fingers)', function() { this.timeout(32); // ms - let core = new InputProcessor(device, '', null, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + let core = new InputProcessor(device, null, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); let context = new SyntheticTextStore("", 0); let keyboard = keyboardWithHarness.activeKeyboard; @@ -160,7 +160,7 @@ describe('InputProcessor', function() { // These often run on VMs, so we'll be a bit generous. // I mean, it IS long context, and time thresholding is disabled within Node. - let core = new InputProcessor(device, '', null, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + let core = new InputProcessor(device, null, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); let keyboard = keyboardWithHarness.activeKeyboard; let layout = keyboard.layout(DeviceSpec.FormFactor.Phone); @@ -179,7 +179,7 @@ describe('InputProcessor', function() { it('with minimal context (with fat-fingers)', function() { this.timeout(32); // ms - let core = new InputProcessor(device, '', null, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + let core = new InputProcessor(device, null, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); let context = new SyntheticTextStore("", 0); let keyboard = keyboardWithHarness.activeKeyboard; @@ -203,7 +203,7 @@ describe('InputProcessor', function() { // 'without fat-fingers' test. // It IS long context, and time thresholding is disabled within Node. - let core = new InputProcessor(device, '', null, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + let core = new InputProcessor(device, null, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); let keyboard = keyboardWithHarness.activeKeyboard; let layout = keyboard.layout(DeviceSpec.FormFactor.Phone); @@ -239,7 +239,7 @@ describe('InputProcessor', function() { for (let testSet of testsToRun.testSet) { it(testSet.msg ?? 'test', function() { this.timeout(32); // ms - let core = new InputProcessor(device, '', null, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + let core = new InputProcessor(device, null, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); let context = new SyntheticTextStore("", 0); let keyboard = keyboardWithHarness.activeKeyboard as JSKeyboard; @@ -309,7 +309,7 @@ describe('InputProcessor', function() { }); it('replaces appended whitespace when a manually-applied suggestion is followed by a K_SPACE (dummy models)', async () => { - const core = new InputProcessor(device, '', nodePredictiveTextWorkerFactory, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + const core = new InputProcessor(device, new NodePredictiveTextWorkerFactory(), {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); const langProcessor = core.languageProcessor; try { @@ -366,7 +366,7 @@ describe('InputProcessor', function() { }); it('replaces appended whitespace when a manually-applied suggestion is followed by a K_SPACE (trie models)', async () => { - const core = new InputProcessor(device, '', nodePredictiveTextWorkerFactory, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + const core = new InputProcessor(device, new NodePredictiveTextWorkerFactory(), {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); const langProcessor = core.languageProcessor; try { @@ -424,7 +424,7 @@ describe('InputProcessor', function() { }); it('auto-applies a suggestion properly when available and triggered appropriately', async () => { - const core = new InputProcessor(device, '', nodePredictiveTextWorkerFactory, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + const core = new InputProcessor(device, new NodePredictiveTextWorkerFactory(), {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); const langProcessor = core.languageProcessor; try { @@ -476,7 +476,7 @@ describe('InputProcessor', function() { }); it('displays a reversion after manually applying a suggestion and immediately backspacing', async () => { - const core = new InputProcessor(device, '', nodePredictiveTextWorkerFactory, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + const core = new InputProcessor(device, new NodePredictiveTextWorkerFactory(), {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); const langProcessor = core.languageProcessor; try { @@ -533,7 +533,7 @@ describe('InputProcessor', function() { }); it('displays a reversion after returning to the whitespace after a manually-applied suggestion', async () => { - const core = new InputProcessor(device, '', nodePredictiveTextWorkerFactory, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + const core = new InputProcessor(device, new NodePredictiveTextWorkerFactory(), {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); const langProcessor = core.languageProcessor; try { @@ -594,7 +594,7 @@ describe('InputProcessor', function() { }); it("displays a reversion after returning to the end of a manually-applied suggestion's body", async () => { - const core = new InputProcessor(device, '', nodePredictiveTextWorkerFactory, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + const core = new InputProcessor(device, new NodePredictiveTextWorkerFactory(), {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); const langProcessor = core.languageProcessor; try { @@ -656,7 +656,7 @@ describe('InputProcessor', function() { }); it("does not display a reversion after backspacing part of an applied suggestion", async () => { - const core = new InputProcessor(device, '', nodePredictiveTextWorkerFactory, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + const core = new InputProcessor(device, new NodePredictiveTextWorkerFactory(), {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); const langProcessor = core.languageProcessor; try { @@ -720,7 +720,7 @@ describe('InputProcessor', function() { }); it('displays a reversion after auto-applying a suggestion and immediately backspacing', async () => { - const core = new InputProcessor(device, '', nodePredictiveTextWorkerFactory, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + const core = new InputProcessor(device, new NodePredictiveTextWorkerFactory(), {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); const langProcessor = core.languageProcessor; try { @@ -775,7 +775,7 @@ describe('InputProcessor', function() { }); it('displays a reversion after returning to the whitespace after a auto-applied suggestion', async () => { - const core = new InputProcessor(device, '', nodePredictiveTextWorkerFactory, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + const core = new InputProcessor(device, new NodePredictiveTextWorkerFactory(), {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); const langProcessor = core.languageProcessor; try { @@ -829,7 +829,7 @@ describe('InputProcessor', function() { }); it("displays a reversion after returning to the end of an auto-applied suggestion's body", async () => { - const core = new InputProcessor(device, '', nodePredictiveTextWorkerFactory, {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); + const core = new InputProcessor(device, new NodePredictiveTextWorkerFactory(), {...DEFAULT_PROCESSOR_INIT_OPTIONS, keyboardInterface: keyboardWithHarness}); const langProcessor = core.languageProcessor; try { diff --git a/web/src/test/auto/headless/engine/main/languageProcessor.tests.ts b/web/src/test/auto/headless/engine/main/languageProcessor.tests.ts index 83360e8aebc..b23f557b572 100644 --- a/web/src/test/auto/headless/engine/main/languageProcessor.tests.ts +++ b/web/src/test/auto/headless/engine/main/languageProcessor.tests.ts @@ -8,7 +8,7 @@ import path from 'node:path'; import { assert } from 'chai'; -import { nodePredictiveTextWorkerFactory } from "@keymanapp/lexical-model-layer/node"; +import { NodePredictiveTextWorkerFactory } from "@keymanapp/lexical-model-layer/node"; import { LexicalModelCompiler } from '@keymanapp/kmc-model'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { SyntheticTextStore } from 'keyman/engine/keyboard'; @@ -16,6 +16,7 @@ import { LanguageProcessor, TranscriptionCache } from 'keyman/engine/main'; import { ModelSpec } from 'keyman/engine/interfaces'; import { MinimalKeymanGlobal } from 'keyman/engine/keyboard'; +import { getPredictiveTextWorkerPath } from 'keyman/test/resources'; const KEYMAN_ROOT = env.KEYMAN_ROOT; @@ -35,7 +36,7 @@ describe('LanguageProcessor', function() { const callbacks = new TestCompilerCallbacks(this); beforeEach(function() { - languageProcessor = new LanguageProcessor(nodePredictiveTextWorkerFactory, '', new TranscriptionCache()); + languageProcessor = new LanguageProcessor(new NodePredictiveTextWorkerFactory(getPredictiveTextWorkerPath()), new TranscriptionCache()); }); afterEach(function() { @@ -54,7 +55,7 @@ describe('LanguageProcessor', function() { constructInstance() { throw new Error("Simulating a platform without Worker support"); } - }, '', new TranscriptionCache()); + }, new TranscriptionCache()); assert.isFalse(workerlessProcessor.canEnable); } catch { assert.fail('Worker construction error was unhandled'); diff --git a/web/src/test/auto/resources/index.ts b/web/src/test/auto/resources/index.ts index 7a770a9f3df..4940d95643d 100644 --- a/web/src/test/auto/resources/index.ts +++ b/web/src/test/auto/resources/index.ts @@ -17,5 +17,9 @@ export function getKeymanRoot(): string { } export function getWebTestResourcesPath(): string { - return getKeymanRoot() + '/web/src/test/auto/resources'; + return `${getKeymanRoot()}web/src/test/auto/resources`; } + +export function getPredictiveTextWorkerPath(): string { + return `${getKeymanRoot()}web/build/publish/debug/worker-thread.js`; +} \ No newline at end of file From d296b53cbd58d3466302a5f83660de47936e520e Mon Sep 17 00:00:00 2001 From: Joshua Horton Date: Wed, 23 Sep 2026 11:03:36 -0500 Subject: [PATCH 5/5] fix(ios): restore ios engine worker sourcemapping --- .../Classes/Resource Management/Storage.swift | 8 ++++++++ ios/engine/build.sh | 2 ++ 2 files changed, 10 insertions(+) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/Storage.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/Storage.swift index 8ac69704cc2..3c5e4f3dac5 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/Storage.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/Storage.swift @@ -238,10 +238,18 @@ extension Storage { resourceName: "keymanweb-webview.js", dstDir: baseDir, excludeFromBackup: true) + try Storage.copy(from: bundle, + resourceName: "keymanweb-webview.js.map", + dstDir: baseDir, + excludeFromBackup: true) try Storage.copy(from: bundle, resourceName: "worker-thread.js", dstDir: baseDir, excludeFromBackup: true) + try Storage.copy(from: bundle, + resourceName: "worker-thread.js.map", + dstDir: baseDir, + excludeFromBackup: true) try Storage.copy(from: bundle, resourceName: "keyman-sentry.js", dstDir: baseDir, diff --git a/ios/engine/build.sh b/ios/engine/build.sh index fe3e38b216c..f87fe2262fc 100755 --- a/ios/engine/build.sh +++ b/ios/engine/build.sh @@ -109,7 +109,9 @@ function update_bundle ( ) { cp "$KMW_RESOURCES/osk/kmwosk.css" "$BUNDLE_PATH/kmwosk.css" cp "$KMW_RESOURCES/osk/keymanweb-osk.ttf" "$BUNDLE_PATH/keymanweb-osk.ttf" cp "$KMW_PRODUCT/keymanweb-webview.js" "$BUNDLE_PATH/keymanweb-webview.js" + cp "$KMW_PRODUCT/keymanweb-webview.js.map" "$BUNDLE_PATH/keymanweb-webview.js.map" cp "$KMW_PRODUCT/worker-thread.js" "$BUNDLE_PATH/worker-thread.js" + cp "$KMW_PRODUCT/worker-thread.js.map" "$BUNDLE_PATH/worker-thread.js.map" cp "$KEYMAN_ROOT/common/web/sentry-manager/build/lib/index.js" "$BUNDLE_PATH/keyman-sentry.js" }