mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-22 05:47:41 +00:00
Merge pull request #13290 from keymanapp/fix/web/workerless-engine-support
fix(web): properly support workerless use of Web engine
This commit is contained in:
commit
69c80d31e4
13 changed files with 84 additions and 37 deletions
|
|
@ -13,4 +13,4 @@ import { SourcemappedWorker } from '@keymanapp/lexical-model-layer/web'
|
|||
var sPath = ss.substr(0,ss.lastIndexOf('/')+1);
|
||||
|
||||
// @ts-ignore
|
||||
window['keyman'] = new KeymanEngine(SourcemappedWorker.constructInstance(), sPath);
|
||||
window['keyman'] = new KeymanEngine(SourcemappedWorker, sPath);
|
||||
|
|
@ -27,6 +27,7 @@ import { UIModule } from './uiModuleInterface.js';
|
|||
import { HotkeyManager } from './hotkeyManager.js';
|
||||
import { BeepHandler } from './beepHandler.js';
|
||||
import KeyboardInterface from './keyboardInterface.js';
|
||||
import { WorkerFactory } from '@keymanapp/lexical-model-layer/web';
|
||||
|
||||
export default class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, ContextManager, HardwareEventKeyboard> {
|
||||
touchLanguageMenu?: LanguageMenu;
|
||||
|
|
@ -55,10 +56,10 @@ export default class KeymanEngine extends KeymanEngineBase<BrowserConfiguration,
|
|||
this.contextManager.restoreLastActiveTarget();
|
||||
}
|
||||
|
||||
constructor(worker: Worker, sourceUri: string) {
|
||||
constructor(workerFactory: WorkerFactory, sourceUri: string) {
|
||||
const config = new BrowserConfiguration(sourceUri); // currently set to perform device auto-detect.
|
||||
|
||||
super(worker, config, new ContextManager(config, () => this.legacyAPIEvents), (engine) => {
|
||||
super(workerFactory, config, new ContextManager(config, () => this.legacyAPIEvents), (engine) => {
|
||||
return {
|
||||
// The `engine` parameter cannot be supplied with the constructing instance before calling
|
||||
// `super`, hence the 'fun' rigging to supply it _from_ `super` via this closure.
|
||||
|
|
|
|||
|
|
@ -13,4 +13,4 @@ import { Worker } from '@keymanapp/lexical-model-layer/web'
|
|||
var sPath = ss.substr(0,ss.lastIndexOf('/')+1);
|
||||
|
||||
// @ts-ignore
|
||||
window['keyman'] = new KeymanEngine(Worker.constructInstance(), sPath);
|
||||
window['keyman'] = new KeymanEngine(Worker, sPath);
|
||||
|
|
@ -13,4 +13,4 @@ const ss = scripts[scripts.length-1].src;
|
|||
const sPath = ss.substring(0,ss.lastIndexOf('/')+1);
|
||||
|
||||
// @ts-ignore
|
||||
window['keyman'] = new KeymanEngine(SourcemappedWorker.constructInstance(), sPath);
|
||||
window['keyman'] = new KeymanEngine(SourcemappedWorker, sPath);
|
||||
|
|
@ -9,12 +9,13 @@ import { WebviewConfiguration, WebviewInitOptionDefaults, WebviewInitOptionSpec
|
|||
import ContextManager, { ContextHost } from './contextManager.js';
|
||||
import PassthroughKeyboard from './passthroughKeyboard.js';
|
||||
import { buildEmbeddedGestureConfig, setupEmbeddedListeners } from './oskConfiguration.js';
|
||||
import { WorkerFactory } from '@keymanapp/lexical-model-layer';
|
||||
|
||||
export default class KeymanEngine extends KeymanEngineBase<WebviewConfiguration, ContextManager, PassthroughKeyboard> {
|
||||
// Ideally, we would be able to auto-detect `sourceUri`: https://stackoverflow.com/a/60244278.
|
||||
// But it's too new of a feature to utilize... and also expects to be in a module, when this may
|
||||
// be compiled down to an IIFE.
|
||||
constructor(worker: Worker, sourceUri: string) {
|
||||
constructor(workerFactory: WorkerFactory, sourceUri: string) {
|
||||
const config = new WebviewConfiguration(sourceUri); // currently set to perform device auto-detect.
|
||||
|
||||
config.onRuleFinalization = (ruleBehavior: RuleBehavior) => {
|
||||
|
|
@ -30,7 +31,7 @@ export default class KeymanEngine extends KeymanEngineBase<WebviewConfiguration,
|
|||
}
|
||||
}
|
||||
|
||||
super(worker, config, new ContextManager(config), (engine) => {
|
||||
super(workerFactory, config, new ContextManager(config), (engine) => {
|
||||
return {
|
||||
// The `engine` parameter cannot be supplied with the constructing instance before calling
|
||||
// `super`, hence the 'fun' rigging to supply it _from_ `super` via this closure.
|
||||
|
|
|
|||
|
|
@ -13,4 +13,4 @@ const ss = scripts[scripts.length-1].src;
|
|||
const sPath = ss.substring(0,ss.lastIndexOf('/')+1);
|
||||
|
||||
// @ts-ignore
|
||||
window['keyman'] = new KeymanEngine(Worker.constructInstance(), sPath);
|
||||
window['keyman'] = new KeymanEngine(Worker, sPath);
|
||||
|
|
@ -20,6 +20,7 @@ import {
|
|||
|
||||
import { TranscriptionCache } from "./transcriptionCache.js";
|
||||
import { LexicalModelTypes } from '@keymanapp/common-types';
|
||||
import { WorkerFactory } from "@keymanapp/lexical-model-layer";
|
||||
|
||||
export class InputProcessor {
|
||||
public static readonly DEFAULT_OPTIONS: ProcessorInitOptions = {
|
||||
|
|
@ -37,7 +38,7 @@ export class InputProcessor {
|
|||
|
||||
private readonly contextCache = new TranscriptionCache();
|
||||
|
||||
constructor(device: DeviceSpec, predictiveTextWorker: Worker, options?: ProcessorInitOptions) {
|
||||
constructor(device: DeviceSpec, predictiveWorkerFactory: WorkerFactory, options?: ProcessorInitOptions) {
|
||||
if(!device) {
|
||||
throw new Error('device must be defined');
|
||||
}
|
||||
|
|
@ -48,7 +49,7 @@ export class InputProcessor {
|
|||
|
||||
this.contextDevice = device;
|
||||
this.kbdProcessor = new KeyboardProcessor(device, options);
|
||||
this.lngProcessor = new LanguageProcessor(predictiveTextWorker, this.contextCache);
|
||||
this.lngProcessor = new LanguageProcessor(predictiveWorkerFactory, this.contextCache);
|
||||
}
|
||||
|
||||
public get languageProcessor(): LanguageProcessor {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { EventEmitter } from "eventemitter3";
|
||||
import { LMLayer } from "@keymanapp/lexical-model-layer/web";
|
||||
import { LMLayer, WorkerFactory } from "@keymanapp/lexical-model-layer/web";
|
||||
import { OutputTarget, Transcription, Mock } from "keyman/engine/js-processor";
|
||||
import { LanguageProcessorEventMap, ModelSpec, StateChangeEnum, ReadySuggestions } from 'keyman/engine/interfaces';
|
||||
import ContextWindow from "./contextWindow.js";
|
||||
|
|
@ -25,7 +25,7 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
|
|||
|
||||
private _state: StateChangeEnum = 'inactive';
|
||||
|
||||
public constructor(predictiveTextWorker: Worker, transcriptionCache: TranscriptionCache, supportsRightDeletions: boolean = false) {
|
||||
public constructor(predictiveWorkerFactory: WorkerFactory, transcriptionCache: TranscriptionCache, supportsRightDeletions: boolean = false) {
|
||||
super();
|
||||
|
||||
this.recentTranscriptions = transcriptionCache;
|
||||
|
|
@ -38,11 +38,21 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
|
|||
maxRightContextCodePoints: supportsRightDeletions ? 0 : 64
|
||||
}
|
||||
|
||||
if(!predictiveTextWorker) {
|
||||
if(!predictiveWorkerFactory) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lmEngine = new LMLayer(capabilities, predictiveTextWorker);
|
||||
let workerInstance: Worker;
|
||||
try {
|
||||
workerInstance = predictiveWorkerFactory?.constructInstance();
|
||||
} catch(e) {
|
||||
// We can condition on `lmEngine` being null/undefined.
|
||||
console.warn('Web workers are not available: ' + (e ?? '').toString());
|
||||
workerInstance = null;
|
||||
}
|
||||
if(workerInstance) {
|
||||
this.lmEngine = new LMLayer(capabilities, workerInstance);
|
||||
}
|
||||
}
|
||||
|
||||
public get activeModel(): ModelSpec {
|
||||
|
|
@ -58,6 +68,10 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
|
|||
}
|
||||
|
||||
public unloadModel() {
|
||||
if(!this.isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lmEngine.unloadModel();
|
||||
delete this.currentModel;
|
||||
delete this.configuration;
|
||||
|
|
@ -71,6 +85,10 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
|
|||
throw new Error("Null reference not allowed.");
|
||||
}
|
||||
|
||||
if(!this.canEnable) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let specType: 'file'|'raw' = model.path ? 'file' : 'raw';
|
||||
let source = specType == 'file' ? model.path : model.code;
|
||||
|
||||
|
|
@ -108,9 +126,6 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
|
|||
}
|
||||
|
||||
public invalidateContext(outputTarget: OutputTarget, layerId: string): Promise<Suggestion[]> {
|
||||
// Signal to any predictive text UI that the context has changed, invalidating recent predictions.
|
||||
this.emit('invalidatesuggestions', 'context');
|
||||
|
||||
// If there's no active model, there can be no predictions.
|
||||
// We'll also be missing important data needed to even properly REQUEST the predictions.
|
||||
if(!this.currentModel || !this.configuration) {
|
||||
|
|
@ -121,7 +136,12 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
|
|||
// invalidateContext otherwise bypasses .predict()'s check against this.
|
||||
if(!this.isActive) {
|
||||
return Promise.resolve([]);
|
||||
} else if(outputTarget) {
|
||||
}
|
||||
|
||||
// Signal to any predictive text UI that the context has changed, invalidating recent predictions.
|
||||
this.emit('invalidatesuggestions', 'context');
|
||||
|
||||
if(outputTarget) {
|
||||
let transcription = outputTarget.buildTranscriptionFrom(outputTarget, null, false);
|
||||
return this.predict_internal(transcription, true, layerId);
|
||||
} else {
|
||||
|
|
@ -173,6 +193,10 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
|
|||
throw "Accepting suggestions requires a destination OutputTarget instance."
|
||||
}
|
||||
|
||||
if(!this.isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(!this.isConfigured) {
|
||||
// If we're in this state, the suggestion is now outdated; the user must have swapped keyboard and model.
|
||||
console.warn("Could not apply suggestion; the corresponding model has been unloaded");
|
||||
|
|
@ -244,6 +268,10 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
|
|||
throw "Accepting suggestions requires a destination OutputTarget instance."
|
||||
}
|
||||
|
||||
if(!this.isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the state of the context at the time the suggestion was generated.
|
||||
// This may refer to the context before an input keystroke or before application
|
||||
// of a predictive suggestion.
|
||||
|
|
@ -278,7 +306,7 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
|
|||
}
|
||||
|
||||
public predictFromTarget(outputTarget: OutputTarget, layerId: string): Promise<Suggestion[]> {
|
||||
if(!outputTarget) {
|
||||
if(!this.isActive || !outputTarget) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -292,7 +320,7 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
|
|||
* @param transcription The triggering transcription (if it exists)
|
||||
*/
|
||||
private predict_internal(transcription: Transcription, resetContext: boolean, layerId: string): Promise<Suggestion[]> {
|
||||
if(!transcription) {
|
||||
if(!this.isActive || !transcription) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -341,19 +369,19 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
|
|||
}
|
||||
|
||||
public shutdown() {
|
||||
this.lmEngine.shutdown();
|
||||
this.lmEngine?.shutdown();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
public get isActive(): boolean {
|
||||
if(!this.canEnable()) {
|
||||
if(!this.canEnable) {
|
||||
this._mayPredict = false;
|
||||
return false;
|
||||
}
|
||||
return (this.activeModel || false) && this._mayPredict;
|
||||
}
|
||||
|
||||
canEnable(): boolean {
|
||||
public get canEnable(): boolean {
|
||||
// Is not initialized if there is no worker.
|
||||
return !!this.lmEngine;
|
||||
}
|
||||
|
|
@ -363,7 +391,7 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
|
|||
}
|
||||
|
||||
public set mayPredict(flag: boolean) {
|
||||
if(!this.canEnable()) {
|
||||
if(!this.canEnable) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -416,6 +444,10 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
|
|||
}
|
||||
|
||||
public tryAcceptSuggestion(source: string): boolean {
|
||||
if(!this.isActive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The object below is to facilitate a pass-by-reference on the boolean flag,
|
||||
// allowing the event's handler to signal if whitespace has been added via
|
||||
// auto-applied suggestion that should be blocked on the next keystroke.
|
||||
|
|
@ -426,6 +458,10 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
|
|||
}
|
||||
|
||||
public tryRevertSuggestion(): boolean {
|
||||
if(!this.isActive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If and when we do auto-revert, the suggestion is to pass this object to the event and
|
||||
// denote any mutations to the contained value.
|
||||
//let returnObj = {shouldSwallow: false};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { type KeyEvent, type Keyboard, KeyboardKeymanGlobal } from "keyman/engine/keyboard";
|
||||
import { OutputTarget, ProcessorInitOptions, RuleBehavior } from 'keyman/engine/js-processor';
|
||||
import { DOMKeyboardLoader as KeyboardLoader } from "keyman/engine/keyboard/dom-keyboard-loader";
|
||||
import { WorkerFactory } from "@keymanapp/lexical-model-layer/web"
|
||||
import { InputProcessor } from './headless/inputProcessor.js';
|
||||
import { OSKView } from "keyman/engine/osk";
|
||||
import { KeyboardRequisitioner, ModelCache, toUnprefixedKeyboardId as unprefixed } from "keyman/engine/keyboard-storage";
|
||||
|
|
@ -112,7 +113,7 @@ export default class KeymanEngine<
|
|||
* the superclass constructor.
|
||||
*/
|
||||
constructor(
|
||||
worker: Worker,
|
||||
workerFactory: WorkerFactory,
|
||||
config: Configuration,
|
||||
contextManager: ContextManager,
|
||||
processorConfigInitializer: (engine: KeymanEngine<Configuration, ContextManager, HardKeyboard>) => ProcessorConfiguration
|
||||
|
|
@ -123,7 +124,7 @@ export default class KeymanEngine<
|
|||
const processorConfiguration = processorConfigInitializer(this);
|
||||
processorConfiguration.baseLayout = determineBaseLayout();
|
||||
this.interface = processorConfiguration.keyboardInterface as KeyboardInterface<ContextManager>;
|
||||
this.core = new InputProcessor(config.hostDevice, worker, 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,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
export { default as LMLayer } from './lmlayer.js';
|
||||
export { default as NodeWorker } from './node/worker.js';
|
||||
export { default as WebWorker } from './web/worker.js';
|
||||
export { default as WebWorker } from './web/worker.js';
|
||||
|
||||
export interface WorkerFactory {
|
||||
constructInstance(): Worker
|
||||
}
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
export { default as LMLayer } from '../lmlayer.js';
|
||||
export { default as Worker } from './worker.js';
|
||||
export { default as SourcemappedWorker } from './sourcemappedWorker.js';
|
||||
export { default as SourcemappedWorker } from './sourcemappedWorker.js';
|
||||
|
||||
export interface WorkerFactory {
|
||||
constructInstance(): Worker
|
||||
}
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
export { default as LMLayer } from '../lmlayer.js';
|
||||
export { default as Worker } from './worker.js';
|
||||
export { default as SourcemappedWorker } from './sourcemappedWorker.js';
|
||||
export { default as SourcemappedWorker } from './sourcemappedWorker.js';
|
||||
|
||||
export interface WorkerFactory {
|
||||
constructInstance(): Worker
|
||||
}
|
||||
|
|
@ -64,18 +64,17 @@ function dummiedGetLayer() {
|
|||
}
|
||||
|
||||
describe("PredictionContext", () => {
|
||||
let worker;
|
||||
let langProcessor;
|
||||
|
||||
beforeEach(function() {
|
||||
worker = LMWorker.constructInstance();
|
||||
langProcessor = new LanguageProcessor(LMWorker, new TranscriptionCache());
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
worker.terminate();
|
||||
langProcessor.shutdown();
|
||||
});
|
||||
|
||||
it('receives predictions as they are generated', async function () {
|
||||
const langProcessor = new LanguageProcessor(worker, new TranscriptionCache());
|
||||
await langProcessor.loadModel(appleDummyModel); // await: must fully 'configure', load script into worker.
|
||||
|
||||
const predictiveContext = new PredictionContext(langProcessor, dummiedGetLayer);
|
||||
|
|
@ -115,7 +114,6 @@ describe("PredictionContext", () => {
|
|||
});
|
||||
|
||||
it('ignores outdated predictions', async function () {
|
||||
const langProcessor = new LanguageProcessor(worker, new TranscriptionCache());
|
||||
await langProcessor.loadModel(appleDummyModel); // await: must fully 'configure', load script into worker.
|
||||
|
||||
const predictiveContext = new PredictionContext(langProcessor, dummiedGetLayer);
|
||||
|
|
@ -179,7 +177,6 @@ describe("PredictionContext", () => {
|
|||
});
|
||||
|
||||
it('sendUpdateState retrieves the most recent suggestion set', async function() {
|
||||
const langProcessor = new LanguageProcessor(worker, new TranscriptionCache());
|
||||
await langProcessor.loadModel(appleDummyModel); // await: must fully 'configure', load script into worker.
|
||||
|
||||
const predictiveContext = new PredictionContext(langProcessor, dummiedGetLayer);
|
||||
|
|
@ -204,7 +201,6 @@ describe("PredictionContext", () => {
|
|||
});
|
||||
|
||||
it('suggestion application logic & triggered effects', async function () {
|
||||
const langProcessor = new LanguageProcessor(worker, new TranscriptionCache());
|
||||
await langProcessor.loadModel(appleDummyModel); // await: must fully 'configure', load script into worker.
|
||||
|
||||
const predictiveContext = new PredictionContext(langProcessor, dummiedGetLayer);
|
||||
|
|
@ -271,7 +267,6 @@ describe("PredictionContext", () => {
|
|||
});
|
||||
|
||||
it('reversion application logic & triggered effects', async function () {
|
||||
const langProcessor = new LanguageProcessor(worker, new TranscriptionCache());
|
||||
await langProcessor.loadModel(appleDummyModel); // await: must fully 'configure', load script into worker.
|
||||
|
||||
const predictiveContext = new PredictionContext(langProcessor, dummiedGetLayer);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue