change(web): remove .get, force key type to number, base rewind on key value

This commit is contained in:
Joshua Horton 2025-08-18 13:50:49 -05:00
parent 4d66ff671d
commit d6d44c4d1d
7 changed files with 28 additions and 51 deletions

View file

@ -14,10 +14,10 @@
* reaching a specified capacity and that provides the ability to
* forget _all_ entries more recent than a specified entry.
*/
export class RewindableCache<Key, Value> {
export class RewindableCache<Value> {
static readonly DEFAULT_MAX_SIZE = 5;
private map: Map<Key, Value> = new Map();
private map: Map<number, Value> = new Map();
private maxSize: number;
/**
@ -41,37 +41,18 @@ export class RewindableCache<Key, Value> {
* @param key
* @returns
*/
peek(key: Key): Value {
get(key: number): Value {
const entry = this.map.get(key);
return entry;
}
/**
* Fetches the cache entry with the specified key and also updates its
* most recent access time.
* @param key
* @returns
*/
get(key: Key): Value {
const map = this.map;
const entry = map.get(key);
if(!map.has(key)) {
return undefined;
}
map.delete(key);
map.set(key, entry);
return this.peek(key);
}
/**
* Adds the specified key-value pair as a cache entry, overwriting any
* previous value for the specified key.
* @param key
* @returns
*/
add(key: Key, value: Value) {
add(key: number, value: Value) {
const map = this.map;
if(map.has(key)) {
@ -81,7 +62,7 @@ export class RewindableCache<Key, Value> {
map.set(key, value);
if(map.size > this.maxSize) {
const oldest = map.keys().next().value as Key;
const oldest = map.keys().next().value as number;
map.delete(oldest);
map.delete(oldest);
}
@ -95,7 +76,7 @@ export class RewindableCache<Key, Value> {
* Note: returns an Array and is accordingly O(N).
* @returns
*/
keys(): Key[] {
keys(): number[] {
return [...this.map.keys()].reverse();
}
@ -107,30 +88,26 @@ export class RewindableCache<Key, Value> {
}
/**
* Deletes all entries more recent than the specified key, leaving
* Deletes all entries with a key less than the specified key, leaving
* the specified key as the most recent entry.
* @param key
*/
rewindTo(key: Key) {
rewindTo(key: number) {
const map = this.map;
if(!map.has(key)) {
throw new Error("Key not found; cannot clear entries appearing afterward.");
}
let keyFound: boolean = false;
// Iteration: by insertion order
// The most recent entries will appear after the specified key.
for(const k of map.keys()) {
if(!keyFound) {
keyFound = k == key;
continue;
if(k > key) {
// It is safe to delete a key from a Set or Map while iterating over it in ES6+.
// https://stackoverflow.com/a/28306768
map.delete(k);
map.delete(k);
}
// It is safe to delete a key from a Set while iterating over it in ES6+.
// https://stackoverflow.com/a/28306768
map.delete(k);
map.delete(k);
}
}
}

View file

@ -63,7 +63,7 @@ describe("RewindableCache", () => {
cache.add(5, 'e');
assert.sameOrderedMembers([...cache.keys()], [5, 4]);
assert.sameOrderedMembers([...cache.keys().map((k) => cache.peek(k))], ['e', 'd']);
assert.sameOrderedMembers([...cache.keys().map((k) => cache.get(k))], ['e', 'd']);
assert.equal(cache.size, 2);
});
@ -75,12 +75,13 @@ describe("RewindableCache", () => {
cache.add(4, 'd');
cache.add(5, 'e');
cache.get(2);
// make it more recently-accessed, but don't remove it.
cache.rewindTo(3);
assert.sameOrderedMembers([...cache.keys()], [3, 1]);
assert.sameOrderedMembers([...cache.keys().map((k) => cache.peek(k))], ['c', 'a']);
assert.equal(cache.size, 2);
cache.add(2, 'b'); // 2 is still ORIGINALLY older than 3, even if accessed more recently.
assert.sameOrderedMembers([...cache.keys()], [2, 3, 1]);
assert.sameOrderedMembers([...cache.keys().map((k) => cache.get(k))], ['b', 'c', 'a']);
assert.equal(cache.size, 3);
});
it("forgets no entries if invalid key is provided", () => {
@ -102,7 +103,6 @@ describe("RewindableCache", () => {
cache.add(4, 'd'); // 2
cache.add(5, 'e'); // 3
assert.isUndefined(cache.peek(2), 'outdated cache entry should be forgotten');
assert.isUndefined(cache.get(2), 'outdated cache entry should be forgotten');
});
});

View file

@ -103,7 +103,7 @@ export class InputProcessor {
// Support for multitap context reversion; multitap keys should act as if they were
// the first thing typed since `preInput`, the state before the original base key.
if(keyEvent.baseTranscriptionToken) {
const transcription = this.contextCache.peek(keyEvent.baseTranscriptionToken);
const transcription = this.contextCache.get(keyEvent.baseTranscriptionToken);
if(transcription) {
this.contextCache.rewindTo(keyEvent.baseTranscriptionToken);
// Has there been a context change at any point during the multitap? If so, we need

View file

@ -377,7 +377,7 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
* @returns The matching `Transcription`, or `null` none is found.
*/
public getPredictionState(id: number): Transcription {
return this.recentTranscriptions.peek(id);
return this.recentTranscriptions.get(id);
}
public shutdown() {

View file

@ -3,7 +3,7 @@ import { RewindableCache } from "@keymanapp/web-utils";
const TRANSCRIPTION_BUFFER_SIZE = 10;
export class TranscriptionCache extends RewindableCache<number, Transcription> {
export class TranscriptionCache extends RewindableCache<Transcription> {
constructor() {
super(TRANSCRIPTION_BUFFER_SIZE);
}
@ -17,7 +17,7 @@ export class TranscriptionCache extends RewindableCache<number, Transcription> {
const entries = [...this.keys()].map((key) => {
return {
key: key,
entry: this.peek(key)
entry: this.get(key)
}
});

View file

@ -15,7 +15,7 @@ import { ContextState } from './context-state.js';
import { ContextTransition } from './context-transition.js';
export class ContextTracker {
readonly cache = new RewindableCache<number, ContextTransition>(5);
readonly cache = new RewindableCache<ContextTransition>(5);
// Aim: relocate to ContextTransition in some form?
// Or can we split it up in some manner across the different types?
@ -275,7 +275,7 @@ export class ContextTracker {
if(tokenizedPostContext.left.length > 0) {
for(const id of this.cache.keys()) {
const priorMatchState = this.cache.peek(id);
const priorMatchState = this.cache.get(id);
// Skip intermediate multitap-produced contexts.
// When multitapping, we skip all contexts from prior taps within the same interaction,
@ -303,7 +303,7 @@ export class ContextTracker {
if(transitionId !== undefined) {
if(priorMatchState.transitionId != transitionId) {
// Already has a taggedContext.
this.cache.get(transformDistribution.find((entry) => entry.sample.id !== undefined).sample.id);
this.cache.add(priorMatchState.transitionId, priorMatchState);
}
this.cache.add(transitionId, result);
}
@ -332,7 +332,7 @@ export class ContextTracker {
if(key === undefined) {
return undefined;
} else {
return this.cache.peek(key);
return this.cache.get(key);
}
}

View file

@ -293,7 +293,7 @@ export class ModelCompositor {
}
// When the context is tracked, we prefer the tracked information.
let contextMatchFound = this.contextTracker.cache.peek(-reversion.transformId);
let contextMatchFound = this.contextTracker.cache.get(-reversion.transformId);
if(!contextMatchFound) {
return fallbackSuggestions();