From a82993f125f3a4d9660e12f64e044f2230acc9af Mon Sep 17 00:00:00 2001 From: Joshua Horton Date: Tue, 5 Aug 2025 12:01:45 -0500 Subject: [PATCH] refactor(web): improve ContextState construction patterns After proceeding further in development, the only non-initFromReset initialization pattern that appears is constructing a raw instance, then immediately providing a computed tokenization. Thus, accepting the tokenization as an optional parameter will allow us a nice constructor spec for both init styles. --- .../src/main/correction/context-state.ts | 25 ++++++++++++++++--- .../src/main/correction/context-tracker.ts | 1 - .../cases/edit-distance/context-state.js | 19 ++------------ .../cases/edit-distance/context-tracker.js | 12 --------- 4 files changed, 24 insertions(+), 33 deletions(-) diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/context-state.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/context-state.ts index 3c7643b23e..c6042b4ec5 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/context-state.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/context-state.ts @@ -69,13 +69,32 @@ export class ContextState { */ isManuallyApplied?: boolean; + /** + * Deep-copies a prior instance. + * @param stateToClone + */ constructor(stateToClone: ContextState); - constructor(context: Context, model: LexicalModel); - constructor(param1: Context | ContextState, model?: LexicalModel) { + /** + * Initializes a new ContextState instance based on the active model and context. + * + * If a precomputed tokenization of the context (with prior correction-search + * calculation data) is not available, it will be spun up from scratch. + * + * @param context + * @param model + * @param tokenization + */ + constructor(context: Context, model: LexicalModel, tokenization?: ContextTokenization); + constructor(param1: Context | ContextState, model?: LexicalModel, tokenization?: ContextTokenization) { if(!(param1 instanceof ContextState)) { const context = param1; this._context = context; this.model = model; + if(tokenization) { + this.tokenization = tokenization; + } else { + this.initFromReset(); + } } else { const stateToClone = param1; @@ -102,7 +121,7 @@ export class ContextState { * information is available - typically, immediately after engine * initialization or a context reset. */ - initFromReset() { + private initFromReset() { const context = this.context; const lexicalModel = this.model; diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/context-tracker.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/context-tracker.ts index 2fcfdac161..b20c829e28 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/context-tracker.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/context-tracker.ts @@ -463,7 +463,6 @@ export class ContextTracker extends CircularArray { // Assumption: as a caret needs to move to context before any actual transform distributions occur, // this state is only reached on caret moves; thus, transformDistribution is actually just a single null transform. let state = new ContextState(context, model); - state.initFromReset(); this.enqueue(state); return { state, baseState: null, headTokensRemoved: 0, tailTokensAdded: 0 }; } diff --git a/web/src/engine/predictive-text/worker-thread/src/tests/mocha/cases/edit-distance/context-state.js b/web/src/engine/predictive-text/worker-thread/src/tests/mocha/cases/edit-distance/context-state.js index 3ac00011ff..8ffdea5ef6 100644 --- a/web/src/engine/predictive-text/worker-thread/src/tests/mocha/cases/edit-distance/context-state.js +++ b/web/src/engine/predictive-text/worker-thread/src/tests/mocha/cases/edit-distance/context-state.js @@ -19,21 +19,16 @@ describe('ContextState', () => { assert.equal(state.context, context); assert.equal(state.model, plainModel); - assert.isNotOk(state.tokenization); + assert.isOk(state.tokenization); assert.isUndefined(state.isManuallyApplied); assert.isNotOk(state.suggestions); assert.isNotOk(state.appliedSuggestionId); }); - describe('initFromReset', () => { + describe('initializing without prior tokenization', () => { it('', () => { let context = { left: '', right: '' }; let state = new ContextState(context, plainModel); - - assert.isNotOk(state.tokenization); - assert.equal(state.context, context); - - state.initFromReset(); assert.isOk(state.tokenization); assert.equal(state.tokenization.tokens.length, 1); assert.equal(state.tokenization.tail.exampleInput, ''); @@ -42,11 +37,6 @@ describe('ContextState', () => { it('with initial text (without ending whitespace', () => { let context = { left: 'the quick brown fox', right: '' }; let state = new ContextState(context, plainModel); - - assert.isNotOk(state.tokenization); - assert.equal(state.context, context); - - state.initFromReset(); assert.isOk(state.tokenization); assert.equal(state.tokenization.tokens.length, 7); assert.deepEqual(state.tokenization.exampleInput, ['the', ' ', 'quick', ' ', 'brown', ' ', 'fox']); @@ -55,11 +45,6 @@ describe('ContextState', () => { it('with initial text (with ending whitespace', () => { let context = { left: 'the quick brown fox ', right: '' }; let state = new ContextState(context, plainModel); - - assert.isNotOk(state.tokenization); - assert.equal(state.context, context); - - state.initFromReset(); assert.isOk(state.tokenization); assert.equal(state.tokenization.tokens.length, 9); assert.deepEqual(state.tokenization.exampleInput, ['the', ' ', 'quick', ' ', 'brown', ' ', 'fox', ' ', '']); diff --git a/web/src/engine/predictive-text/worker-thread/src/tests/mocha/cases/edit-distance/context-tracker.js b/web/src/engine/predictive-text/worker-thread/src/tests/mocha/cases/edit-distance/context-tracker.js index 3bc068135f..9db2abed9e 100644 --- a/web/src/engine/predictive-text/worker-thread/src/tests/mocha/cases/edit-distance/context-tracker.js +++ b/web/src/engine/predictive-text/worker-thread/src/tests/mocha/cases/edit-distance/context-tracker.js @@ -44,7 +44,6 @@ describe('ContextTracker', function() { let rawTokens = [" ", "apple", " ", "a", " ", "day", " ", "keeps", " ", "the", " ", "doctor"]; let baseState = new ContextState(existingContext, plainModel); - baseState.initFromReset(); let newContextMatch = ContextTracker.attemptMatchContext(newContext, plainModel, baseState, toWrapperDistribution(transform)); assert.isNotNull(newContextMatch?.state); assert.deepEqual(newContextMatch.state.tokenization.tokens.map(token => token.exampleInput), rawTokens); @@ -66,7 +65,6 @@ describe('ContextTracker', function() { let rawTokens = ["apple", " ", "a", " ", "day", " ", "keeps", " ", "the", " ", "doctor"]; let baseState = new ContextState(existingContext, plainModel); - baseState.initFromReset(); let newContextMatch = ContextTracker.attemptMatchContext(newContext, plainModel, baseState, toWrapperDistribution(transform)); assert.isNotNull(newContextMatch?.state); assert.deepEqual(newContextMatch.state.tokenization.tokens.map(token => token.exampleInput), rawTokens); @@ -88,7 +86,6 @@ describe('ContextTracker', function() { let rawTokens = ["an", " ", "apple", " ", "a", " ", "day", " ", "keeps", " ", "the", " ", "doctor"]; let baseState = new ContextState(existingContext, plainModel); - baseState.initFromReset(); let newContextMatch = ContextTracker.attemptMatchContext(newContext, plainModel, baseState, toWrapperDistribution(transform)); assert.isNotNull(newContextMatch?.state); assert.deepEqual(newContextMatch.state.tokenization.tokens.map(token => token.exampleInput), rawTokens); @@ -111,7 +108,6 @@ describe('ContextTracker', function() { let rawTokens = ["an", " ", "apple", " ", "a", " ", "day", " ", "keeps", " ", "the", " ", "doctor", " ", ""]; let baseState = new ContextState(existingContext, plainModel); - baseState.initFromReset(); let newContextMatch = ContextTracker.attemptMatchContext(newContext, plainModel, baseState, toWrapperDistribution(transform)); assert.isNotNull(newContextMatch?.state); assert.deepEqual(newContextMatch.state.tokenization.tokens.map(token => token.exampleInput), rawTokens); @@ -140,7 +136,6 @@ describe('ContextTracker', function() { let rawTokens = ["an", " ", "apple", " ", "a", " ", "day", " ", "keeps", " ", "the", " ", "doctor"]; let baseState = new ContextState(existingContext, plainModel); - baseState.initFromReset(); let newContextMatch = ContextTracker.attemptMatchContext(newContext, plainModel, baseState, toWrapperDistribution(transform)); assert.isOk(newContextMatch?.state); assert.deepEqual(newContextMatch?.state.tokenization.tokens.map(token => token.exampleInput), rawTokens); @@ -164,7 +159,6 @@ describe('ContextTracker', function() { let rawTokens = ["'", "a"]; let baseState = new ContextState(existingContext, plainModel); - baseState.initFromReset(); let newContextMatch = ContextTracker.attemptMatchContext(newContext, plainModel, baseState, toWrapperDistribution(transform)); assert.isNotNull(newContextMatch?.state); assert.deepEqual(newContextMatch.state.tokenization.tokens.map(token => token.exampleInput), rawTokens); @@ -194,7 +188,6 @@ describe('ContextTracker', function() { let rawTokens = ["apple", " ", "a", " ", "day", " ", "keeps", " ", "the", " ", "doctor", " ", ""]; let baseState = new ContextState(existingContext, plainModel); - baseState.initFromReset(); let newContextMatch = ContextTracker.attemptMatchContext(newContext, plainModel, baseState, toWrapperDistribution(transform)); assert.isNotNull(newContextMatch?.state); assert.deepEqual(newContextMatch.state.tokenization.tokens.map(token => token.exampleInput), rawTokens); @@ -224,7 +217,6 @@ describe('ContextTracker', function() { let rawTokens = ["and", " ", ""]; let baseState = new ContextState(existingContext, plainModel); - baseState.initFromReset(); let newContextMatch = ContextTracker.attemptMatchContext( newContext, plainModel, @@ -259,7 +251,6 @@ describe('ContextTracker', function() { let rawTokens = ["apple", " ", "a", " ", "day", " ", "keeps", " ", "the", " ", "doctor", " ", ""]; let baseState = new ContextState(existingContext, plainModel); - baseState.initFromReset(); let newContextMatch = ContextTracker.attemptMatchContext( newContext, plainModel, @@ -287,7 +278,6 @@ describe('ContextTracker', function() { assert.equal(baseContext.left.length, 1); let baseState = new ContextState({ left: "text'" }, plainModel); - baseState.initFromReset(); // Now the actual check. let newContext = models.tokenize(defaultBreaker, { @@ -317,7 +307,6 @@ describe('ContextTracker', function() { let rawTokens = ["an", " ", "apple", " ", "a", " ", "day", " ", "keeps", " ", "the", " ", "doctor"]; let state = new ContextState(context, plainModel); - state.initFromReset(); assert.deepEqual(state.tokenization.tokens.map(token => token.exampleInput), rawTokens); }); @@ -326,7 +315,6 @@ describe('ContextTracker', function() { let rawTokens = ["an", " ", "apple", " ", "a", " ", "day", " ", "keeps", " ", "the", " ", "doctor", " ", ""]; let state = new ContextState(context, plainModel); - state.initFromReset(); assert.deepEqual(state.tokenization.tokens.map(token => token.exampleInput), rawTokens); }); });