Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(web): gracefully disable predictive-text when WebWorkers are unavailable #13270

Open
wants to merge 2 commits into
base: beta
Choose a base branch
from

Conversation

jahorton
Copy link
Contributor

Fixes: #13262

User Testing

TEST_ANDROID_YETI_WITH_WEB: Using an Android device, verify that our test pages operate normally when using the Yeti browser.

  1. Through the play store, install the Yeti browser.
  2. Launch Yeti.
  3. Navigate to this PR's "Test unminified KeymanWeb" test page.
  4. Verify that the page operates normally, displaying our OSK when editing the usual elements, etc.
  5. Navigate to this PR's "Predictive Text: Robust Testing" test page.
  6. Verify that the page does not display the suggestion banner and that predictive text is disabled.
    • The toggles may throw errors, however.

@keymanapp-test-bot keymanapp-test-bot bot added has-user-test user-test-required User tests have not been completed labels Feb 18, 2025
Copy link
Member

@mcdurdin mcdurdin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a little uncomfortable with how lngProcessor has now become optional without any clear maintenance documentation about this -- it feels a bit fragile.

And a question about the worker.

@@ -48,7 +48,11 @@ export class InputProcessor {

this.contextDevice = device;
this.kbdProcessor = new KeyboardProcessor(device, options);
this.lngProcessor = new LanguageProcessor(predictiveTextWorker, this.contextCache);
try {
this.lngProcessor = new LanguageProcessor(predictiveTextWorker, this.contextCache);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This puzzles me, isn't the issue with Yeti that we cannot construct predictiveTextWorker, so wouldn't this go wrong (much) earlier?

Copy link
Contributor Author

@jahorton jahorton Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export default class SourcemappedWorker {
// the only difference to DefaultWorker is that this class uses
// the unminified LM* blobs
static constructInstance(): Worker {
return new Worker(this.asBlobURI(LMLayerWorkerCode));
}

The issue is that we cannot allow line 9 above to run - we can't instantiate the actual Worker. predictiveTextWorker above corresponds to an instance of the class SourcemappedWorker here. I suppose we could add Factory to the variable and parameter names to make the design pattern clearer.

There's also a Node-friendly variant for use in headless testing, which is why the abstraction exists:

export default 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);

MappedWorker there uses Node workers internally, but their spec differs enough from the Web worker type that we needed special handling for it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at where constructInstance() is called, it seems to be called at the very top-level, e.g.

var scripts = document.getElementsByTagName('script');
var ss = scripts[scripts.length-1].src;
var sPath = ss.substr(0,ss.lastIndexOf('/')+1);
// @ts-ignore
window['keyman'] = new KeymanEngine(Worker.constructInstance(), sPath);

So I am not sure how this fix handles that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh. Makes me wonder why it seemed to work with the artificial repro attempt I tried. 🤷 Good point.

@dinakaranr
Copy link

Test Results

I tested this issue with the attached Keyman"18.0.195-beta-test-13270" build(18/02/2025) on Android 14. Here I am sharing my observation.

  • TEST_ANDROID_YETI_WITH_WEB (Passed):
  1. Installed the Yeti browser.
  2. Launch Yeti.
  3. Navigate to this PR's "Test unminified KeymanWeb" test page.
  4. Verified that the page operates normally.
  5. Verified that the OSK is displayed when clicking in the " text area" box.
  6. Verified that the suggestion banner does not appear.
  7. Navigate to this PR's "Predictive Text: Robust Testing" test page.
  8. Verified that the suggestion banner appeared by default.
  9. Verified that the "Predictions & correction" button is showing by default as a "disable" state.
  10. Click on the "disable predictions" and "disable correction" buttons.
  11. Verify that the page does not display the suggestion banner and that predictive text is disabled.
  12. Verified that no errors appeared when clicking the toggle buttons("Predictions & correction)
    Predictive Text: "Robust Testing" test page shows the "suggestion banner" and "predictive text" by default if I click the toggles button then It's disabled.
    It works well. Thank you.

@keymanapp-test-bot keymanapp-test-bot bot removed the user-test-required User tests have not been completed label Feb 18, 2025
@jahorton
Copy link
Contributor Author

I've clarified the test results with @dinakaranr - it sounds like our WebWorker... actually does work with Yeti - at least with its latest version(s). So... that's good to see, but sadly means we don't have a solid repro we can use to actually validate the "no WebWorkers available" codepath. Hmm. Might have to do some research into a better user test.

@mcdurdin
Copy link
Member

Also wondering if the lngProcessor should always exist and just have a 'disabled' state (unless 'inactive' is sufficient)? Feels less fragile than lots of optional tests and encapsulates its behavior better. And it would be cleaner to keep the try/catch as close as possible around the instance construction.

It also looks like LanguageProcessor already has an inactive state where lmEngine may be undefined if predictiveTextWorker param is falsy, can this be used?

@jahorton
Copy link
Contributor Author

jahorton commented Feb 19, 2025

Also wondering if the lngProcessor should always exist and just have a 'disabled' state (unless 'inactive' is sufficient)? Feels less fragile than lots of optional tests and encapsulates its behavior better. And it would be cleaner to keep the try/catch as close as possible around the instance construction.

I opted for the strategy I did because the approach you're suggesting then motivates a question: what should the class's active methods do if called when the engine is disabled? My intuition says "throw errors", and that is prone to its own form of fragility and instability - we'd want to either try-catch them elsewhere or add a lot of if(!lngProcessor.isInactive) checks before references.

I personally felt that relying on the behavior of optionals encapsulated the same logic more cleanly and succinctly while allowing us to get away with a single try-catch.

That said, how would you answer my first paragraph's question? What would be the design for that?

@mcdurdin
Copy link
Member

What should the class's active methods do if called when the engine is disabled?
That said, how would you answer my first paragraph's question? What would be the design for that?

Given that the way the class is being used is to ignore it if it isn't available, I would design the internals of the class that way -- if it is disabled, active methods become a no-op. Allow the class to be interrogated as to its state (in case we need that somewhere; for completeness); document that when the class is in disabled state, actions are a no-op, not an error.

That makes the design explicit, separates the concerns of availability of the worker, and avoids the fragility of having to nullish-check the class every usage.

@jahorton
Copy link
Contributor Author

I've made a replacement PR as #13290.

@jahorton
Copy link
Contributor Author

I've clarified the test results with @dinakaranr - it sounds like our WebWorker... actually does work with Yeti - at least with its latest version(s). So... that's good to see, but sadly means we don't have a solid repro we can use to actually validate the "no WebWorkers available" codepath. Hmm. Might have to do some research into a better user test.

A bit more on this - it's Yeti 1.1 that shows up in the error reports. Meanwhile, Google's Play Store mentions Yeti being at version 1.5. I'm guessing they added WebWorker support after a while.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: No status
3 participants