diff --git a/android/.gitignore b/android/.gitignore index 7c298cad5c3..84e6ca56ada 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/sentry.min.js KMEA/**/assets/keyman-sentry.js KMEA/**/assets/kmwosk.css 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 b0e8661b953..4b57a6a9bc1 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"; @@ -1073,10 +1074,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 14dc821f198..6c8400db423 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/resources/osk/ajax-loader.gif" "$ENGINE_ASSETS/" cp "$KEYMAN_ROOT/web/build/app/resources/osk/kmwosk.css" "$ENGINE_ASSETS/" cp "$KEYMAN_ROOT/web/build/app/resources/osk/globe-hint.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/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/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..3c5e4f3dac5 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,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 2fa7add0828..f87fe2262fc 100755 --- a/ios/engine/build.sh +++ b/ios/engine/build.sh @@ -109,6 +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" } 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 41fc9c5230b..dd72d621987 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-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" + # 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/main.ts similarity index 54% rename from web/src/app/browser/src/debug-main.ts rename to web/src/app/browser/src/main.ts index aa6b23a2a4b..99b57f491af 100644 --- a/web/src/app/browser/src/debug-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 { SourcemappedWorker } 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(SourcemappedWorker, 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/build.sh b/web/src/app/webview/build.sh index dceaf197158..dfc338ac46e 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" \ @@ -83,6 +83,14 @@ compile_and_copy() { "$script" "$sourcemap" --clean done + 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 } 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/browser/src/release-main.ts b/web/src/app/webview/src/main.ts similarity index 54% rename from web/src/app/browser/src/release-main.ts rename to web/src/app/webview/src/main.ts index 3e3a168ccd9..99b57f491af 100644 --- a/web/src/app/browser/src/release-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(new WebPredictiveTextWorkerFactory(sPath + 'worker-thread.js'), sPath); \ No newline at end of file diff --git a/web/src/app/webview/src/release-main.ts b/web/src/app/webview/src/release-main.ts deleted file mode 100644 index 3e3a168ccd9..00000000000 --- a/web/src/app/webview/src/release-main.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { KeymanEngine } from './keymanEngine.js' -import { WebWorker } 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(WebWorker, 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 ac6d2a670b0..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.wrapped.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..39eb7f39430 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 } 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/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 dc5957207b4..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 @@ -1,7 +1,7 @@ -export { LMLayer } from '../lmlayer.js'; -export { NodeWorker } from './node-worker.js'; -export { SourcemappedWorker } from './sourcemappedWorker.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 } 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/mappedWorker.ts b/web/src/engine/predictive-text/worker-main/src/node/node-predictive-text-worker.ts similarity index 68% 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 8b3d9d2b7b9..ef1928a4b2c 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 @@ -1,3 +1,10 @@ +/* + * 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. /// @@ -7,10 +14,13 @@ import * as worker from 'node:worker_threads'; import { Buffer } from 'node:buffer'; 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 = ` +const nodeWorkerToWebWorkerMappingSource = (filename: string) => (` import { parentPort } from 'node:worker_threads'; import fs from 'node:fs'; import vm from 'node:vm'; @@ -42,37 +52,24 @@ 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(${JSON.stringify(filename)}); +`); /** * 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); +export class NodePredictiveTextWorker extends worker.Worker implements Worker { + constructor(workerFilename: string) { + const buffer = Buffer.from(nodeWorkerToWebWorkerMappingSource(workerFilename)); 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.ts b/web/src/engine/predictive-text/worker-main/src/node/node-worker.ts deleted file mode 100644 index e773ea9ffa4..00000000000 --- a/web/src/engine/predictive-text/worker-main/src/node/node-worker.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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"; - -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); - } -} \ 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..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 @@ -1,7 +1,7 @@ -export { LMLayer } from '../lmlayer.js'; -export { WebWorker } from './web-worker.js'; -export { SourcemappedWorker } from './sourcemappedWorker.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 } from './web-predictive-text-worker-factory.js'; 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-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.ts b/web/src/engine/predictive-text/worker-main/src/web/web-worker.ts deleted file mode 100644 index 8e2a8edbbd3..00000000000 --- a/web/src/engine/predictive-text/worker-main/src/web/web-worker.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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)); - } - - /** - * 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); - } -} \ 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..76b79f030d5 --- /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(): 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 d546edb7040..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, SourcemappedWorker 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 = (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 70583a978ee..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, SourcemappedWorker 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 = (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 df5cb45e3bc..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,47 +1,18 @@ 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'; +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, WebWorker.constructInstance(), true); + let lmLayer = new LMLayer(defaultCapabilities, (new WebPredictiveTextWorkerFactory(predictiveTextWorkerFilename)).constructInstance(), 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..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, 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'; +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' }; @@ -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, (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, WebWorker.constructInstance()); + 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 bf8d0aef5c8..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, 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'; +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' }; @@ -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, (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, WebWorker.constructInstance(), /* 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/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..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.wrapped.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,15 +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 - 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-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 af87bce8114..b5b855664d3 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-thread.js", "exports": { - "./worker-main.wrapped.js": { - "types": "./build/lib/worker-main.wrapped.d.ts", - "default": "./build/lib/worker-main.wrapped.js" + "./worker-thread.js": { + "types": "./build/lib/worker-thread.d.ts", + "default": "./build/lib/worker-thread.js" }, - "./worker-main.wrapped.min.js": { - "types": "./build/lib/worker-main.wrapped.d.ts", - "default": "./build/lib/worker-main.wrapped.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 fcae30e7623..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,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 one makes multiple subsequent calls across the PredictiveTextWorker 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('/engine/predictive-text/worker-thread/build/lib/worker-thread.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/engine/src/main/headless/inputProcessor.ts b/web/src/engine/src/main/headless/inputProcessor.ts index b2f4531a5b0..103ae9ac1bc 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). diff --git a/web/src/engine/src/main/headless/languageProcessor.ts b/web/src/engine/src/main/headless/languageProcessor.ts index 20515c22885..0107d364f99 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"; diff --git a/web/src/engine/src/main/keymanEngineBase.ts b/web/src/engine/src/main/keymanEngineBase.ts index 2ee22a2b370..1318818c288 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 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..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 { 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; +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(NodeWorker, 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 d76b774d61a..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 { 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'; @@ -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, new 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.wrapped.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'); @@ -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, 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, NodeWorker, {...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, NodeWorker, {...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, NodeWorker, {...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, NodeWorker, {...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, NodeWorker, {...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, NodeWorker, {...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, NodeWorker, {...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, NodeWorker, {...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, NodeWorker, {...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 126feab306f..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 { SourcemappedWorker 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'; @@ -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(LMWorker, new TranscriptionCache()); + languageProcessor = new LanguageProcessor(new NodePredictiveTextWorkerFactory(getPredictiveTextWorkerPath()), new TranscriptionCache()); }); afterEach(function() { 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 { 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