@atlaskit/editor-plugin-autocomplete 3.2.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/cjs/pm-plugins/autocomplete-plugin.js +162 -22
- package/dist/cjs/pm-plugins/local-slow-lane-client.js +108 -17
- package/dist/es2019/pm-plugins/autocomplete-plugin.js +148 -22
- package/dist/es2019/pm-plugins/local-slow-lane-client.js +105 -16
- package/dist/esm/pm-plugins/autocomplete-plugin.js +162 -22
- package/dist/esm/pm-plugins/local-slow-lane-client.js +108 -17
- package/package.json +2 -2
- package/src/pm-plugins/autocomplete-plugin.ts +170 -25
- package/src/pm-plugins/local-slow-lane-client.ts +129 -21
|
@@ -4,12 +4,26 @@ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
|
4
4
|
import { keydownHandler } from '@atlaskit/editor-prosemirror/keymap';
|
|
5
5
|
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
|
|
6
6
|
import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
|
|
7
|
+
import { isAutocompleteDebugEnabled } from './debug-mode';
|
|
7
8
|
import { createGhostTextDecorationSet } from './ghost-text-decoration';
|
|
8
9
|
import { createLocalSlowLaneClient } from './local-slow-lane-client';
|
|
9
10
|
import { createSlowLaneClient, setDefaultSlowLaneClient, isWordBoundary } from './slow-lane-client';
|
|
10
11
|
import { predict, loadDefaultVocabulary, loadVectorsAsync, incrementSessionFreq, ingestDocumentPage } from './text-predictor';
|
|
11
12
|
export const autocompletePluginKey = new PluginKey('autocomplete');
|
|
12
13
|
const DEBOUNCE_MS = 150;
|
|
14
|
+
const NETWORK_SLOW_LANE_DEBOUNCE_MS = 300;
|
|
15
|
+
const LOCAL_SLOW_LANE_DEBOUNCE_MS = 100;
|
|
16
|
+
const CONTEXT_REFRESH_THROTTLE_MS = 1000;
|
|
17
|
+
// Caps the dedup Set so long editing sessions don't retain every distinct
|
|
18
|
+
// version of the (potentially hundreds-of-KB) page content for the plugin's
|
|
19
|
+
// lifetime. Eviction is FIFO; a re-ingest of an evicted text is harmless.
|
|
20
|
+
const MAX_INGESTED_CONTEXT_TEXTS = 50;
|
|
21
|
+
// Caps how many times the word-boundary path will retry getContext() while the
|
|
22
|
+
// parent comment is still missing. Combined with the 1s throttle this gives a
|
|
23
|
+
// ~5s window to cover a still-loading comment thread, then stops permanently so
|
|
24
|
+
// non-comment editors (where parentCommentContent never arrives) don't refetch
|
|
25
|
+
// on every word boundary for the plugin's lifetime.
|
|
26
|
+
const MAX_CONTEXT_REFRESH_ATTEMPTS = 5;
|
|
13
27
|
const hasDestroy = client => 'destroy' in client && typeof client.destroy === 'function';
|
|
14
28
|
const createInitialState = () => ({
|
|
15
29
|
ghostText: '',
|
|
@@ -176,6 +190,12 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
176
190
|
let debounceTimer = null;
|
|
177
191
|
let hasIngestedPage = false;
|
|
178
192
|
let resolvedContext;
|
|
193
|
+
/**
|
|
194
|
+
* Kept in sync with the live EditorView so the async getContext() promise
|
|
195
|
+
* can re-trigger a slow-lane update the moment context arrives, even if the
|
|
196
|
+
* user has already typed several words before the promise resolved.
|
|
197
|
+
*/
|
|
198
|
+
let currentView = null;
|
|
179
199
|
/**
|
|
180
200
|
* Set after accepting a suggestion so the next doc-change update
|
|
181
201
|
* skips scheduling a new prediction for the just-inserted text.
|
|
@@ -219,12 +239,115 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
219
239
|
});
|
|
220
240
|
};
|
|
221
241
|
const slowLaneClient = options !== null && options !== void 0 && options.useLocalModel ? createLocalSlowLaneClient({
|
|
222
|
-
debounceMs:
|
|
242
|
+
debounceMs: LOCAL_SLOW_LANE_DEBOUNCE_MS
|
|
223
243
|
}) : createSlowLaneClient({
|
|
224
244
|
baseUrl: '',
|
|
225
|
-
debounceMs:
|
|
245
|
+
debounceMs: NETWORK_SLOW_LANE_DEBOUNCE_MS
|
|
226
246
|
});
|
|
227
247
|
setDefaultSlowLaneClient(slowLaneClient);
|
|
248
|
+
let contextRequestInFlight = false;
|
|
249
|
+
let lastContextRefreshAt = 0;
|
|
250
|
+
// Bounds the word-boundary retry loop so it terminates even when the editor is
|
|
251
|
+
// not in a comment thread (parentCommentContent never resolves).
|
|
252
|
+
let wordBoundaryRefreshAttempts = 0;
|
|
253
|
+
// Set when the plugin is torn down so in-flight getContext() resolutions don't
|
|
254
|
+
// mutate the global text-predictor state after destruction.
|
|
255
|
+
let destroyed = false;
|
|
256
|
+
const ingestedContextTexts = new Set();
|
|
257
|
+
const logContextResolved = (source, context) => {
|
|
258
|
+
var _context$parentCommen, _context$siblingComme4, _context$siblingComme5;
|
|
259
|
+
if (!isAutocompleteDebugEnabled()) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// eslint-disable-next-line no-console
|
|
264
|
+
console.log('%c[Autocomplete] %cgetContext resolved', 'color: #00b8d9; font-weight: bold;', 'color: inherit;', {
|
|
265
|
+
source,
|
|
266
|
+
hasParentComment: !!(context !== null && context !== void 0 && context.parentCommentContent),
|
|
267
|
+
parentCommentPreview: context === null || context === void 0 ? void 0 : (_context$parentCommen = context.parentCommentContent) === null || _context$parentCommen === void 0 ? void 0 : _context$parentCommen.slice(0, 80),
|
|
268
|
+
siblingCount: (_context$siblingComme4 = context === null || context === void 0 ? void 0 : (_context$siblingComme5 = context.siblingCommentsContents) === null || _context$siblingComme5 === void 0 ? void 0 : _context$siblingComme5.length) !== null && _context$siblingComme4 !== void 0 ? _context$siblingComme4 : 0,
|
|
269
|
+
hasFullPage: !!(context !== null && context !== void 0 && context.fullPageContent)
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
const applyContext = context => {
|
|
273
|
+
// Merge rather than replace: the word-boundary retry may resolve only a
|
|
274
|
+
// late-arriving field (e.g. parentCommentContent) without re-sending
|
|
275
|
+
// fullPageContent, so replacing would drop previously resolved context.
|
|
276
|
+
// Strip undefined values first so a field explicitly set to undefined by
|
|
277
|
+
// getContext doesn't overwrite a previously resolved value.
|
|
278
|
+
const definedContext = Object.fromEntries(Object.entries(context).filter(([, value]) => value !== undefined));
|
|
279
|
+
resolvedContext = {
|
|
280
|
+
...resolvedContext,
|
|
281
|
+
...definedContext
|
|
282
|
+
};
|
|
283
|
+
const ingestContextText = text => {
|
|
284
|
+
if (!text || ingestedContextTexts.has(text)) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
ingestedContextTexts.add(text);
|
|
288
|
+
// Evict oldest entries (Set preserves insertion order) to bound memory.
|
|
289
|
+
while (ingestedContextTexts.size > MAX_INGESTED_CONTEXT_TEXTS) {
|
|
290
|
+
const oldest = ingestedContextTexts.values().next().value;
|
|
291
|
+
if (oldest === undefined) {
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
ingestedContextTexts.delete(oldest);
|
|
295
|
+
}
|
|
296
|
+
ingestDocumentPage(text);
|
|
297
|
+
};
|
|
298
|
+
ingestContextText(context.fullPageContent);
|
|
299
|
+
ingestContextText(context.parentCommentContent);
|
|
300
|
+
for (const siblingCommentContent of (_context$siblingComme6 = context.siblingCommentsContents) !== null && _context$siblingComme6 !== void 0 ? _context$siblingComme6 : []) {
|
|
301
|
+
var _context$siblingComme6;
|
|
302
|
+
ingestContextText(siblingCommentContent);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Context arrived after word boundaries may already have fired. Re-send
|
|
306
|
+
// slow-lane context immediately so the next inference includes the thread.
|
|
307
|
+
if (currentView) {
|
|
308
|
+
slowLaneClient.updateContext(buildSlowLaneText(currentView.state.doc.textContent, resolvedContext));
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Returns true when a fetch was actually started, false when it was skipped
|
|
314
|
+
* (no getContext, a request already in flight, or throttled). Callers that
|
|
315
|
+
* track a retry budget should only count attempts where this returns true.
|
|
316
|
+
*/
|
|
317
|
+
const refreshContext = ({
|
|
318
|
+
source,
|
|
319
|
+
allowThrottle = true
|
|
320
|
+
}) => {
|
|
321
|
+
if (!(options !== null && options !== void 0 && options.getContext) || contextRequestInFlight) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
const now = Date.now();
|
|
325
|
+
if (allowThrottle && now - lastContextRefreshAt < CONTEXT_REFRESH_THROTTLE_MS) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
contextRequestInFlight = true;
|
|
329
|
+
lastContextRefreshAt = now;
|
|
330
|
+
options.getContext().then(context => {
|
|
331
|
+
// Bail if the plugin was destroyed while the fetch was in flight —
|
|
332
|
+
// applyContext mutates global text-predictor state we must not touch
|
|
333
|
+
// after teardown.
|
|
334
|
+
if (destroyed) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
logContextResolved(source, context);
|
|
338
|
+
if (!context) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
applyContext(context);
|
|
342
|
+
}).catch(error => {
|
|
343
|
+
logException(error, {
|
|
344
|
+
location: 'editor-plugin-autocomplete/getContext'
|
|
345
|
+
});
|
|
346
|
+
}).finally(() => {
|
|
347
|
+
contextRequestInFlight = false;
|
|
348
|
+
});
|
|
349
|
+
return true;
|
|
350
|
+
};
|
|
228
351
|
|
|
229
352
|
/**
|
|
230
353
|
* Schedule a prediction after a short debounce.
|
|
@@ -403,26 +526,10 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
403
526
|
});
|
|
404
527
|
if (!hasIngestedPage) {
|
|
405
528
|
hasIngestedPage = true;
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
resolvedContext = context;
|
|
413
|
-
if (context.fullPageContent) {
|
|
414
|
-
ingestDocumentPage(context.fullPageContent);
|
|
415
|
-
}
|
|
416
|
-
if (context.parentCommentContent) {
|
|
417
|
-
ingestDocumentPage(context.parentCommentContent);
|
|
418
|
-
}
|
|
419
|
-
(_context$siblingComme4 = context.siblingCommentsContents) === null || _context$siblingComme4 === void 0 ? void 0 : _context$siblingComme4.forEach(ingestDocumentPage);
|
|
420
|
-
}).catch(error => {
|
|
421
|
-
logException(error, {
|
|
422
|
-
location: 'editor-plugin-autocomplete/getContext'
|
|
423
|
-
});
|
|
424
|
-
});
|
|
425
|
-
}
|
|
529
|
+
refreshContext({
|
|
530
|
+
source: 'focus',
|
|
531
|
+
allowThrottle: false
|
|
532
|
+
});
|
|
426
533
|
}
|
|
427
534
|
return false;
|
|
428
535
|
}
|
|
@@ -430,6 +537,7 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
430
537
|
},
|
|
431
538
|
view: () => ({
|
|
432
539
|
update: (view, prevState) => {
|
|
540
|
+
currentView = view;
|
|
433
541
|
if (!prevState.doc.eq(view.state.doc)) {
|
|
434
542
|
if (justAccepted) {
|
|
435
543
|
justAccepted = false;
|
|
@@ -445,18 +553,36 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
445
553
|
maybeUpdateSessionFrequency(view, prevState);
|
|
446
554
|
const textBefore = getTextBeforeCursor(view.state);
|
|
447
555
|
if (isWordBoundary(textBefore)) {
|
|
556
|
+
var _resolvedContext;
|
|
448
557
|
slowLaneClient.updateContext(buildSlowLaneText(view.state.doc.textContent, resolvedContext));
|
|
558
|
+
|
|
559
|
+
// Context may not have resolved on first focus (e.g. comment
|
|
560
|
+
// thread still loading). Retry on word boundaries until we have
|
|
561
|
+
// the parent comment, throttled so we don't refetch constantly
|
|
562
|
+
// and capped so non-comment editors stop retrying entirely.
|
|
563
|
+
if (!((_resolvedContext = resolvedContext) !== null && _resolvedContext !== void 0 && _resolvedContext.parentCommentContent) && wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS) {
|
|
564
|
+
// Only count the attempt when a fetch actually started, so an
|
|
565
|
+
// in-flight or throttled no-op doesn't burn the retry budget.
|
|
566
|
+
if (refreshContext({
|
|
567
|
+
source: 'word-boundary'
|
|
568
|
+
})) {
|
|
569
|
+
wordBoundaryRefreshAttempts++;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
449
572
|
}
|
|
450
573
|
schedulePrediction(view);
|
|
451
574
|
}
|
|
452
575
|
},
|
|
453
576
|
destroy: () => {
|
|
577
|
+
destroyed = true;
|
|
578
|
+
currentView = null;
|
|
454
579
|
if (debounceTimer) {
|
|
455
580
|
clearTimeout(debounceTimer);
|
|
456
581
|
}
|
|
457
582
|
if (hasDestroy(slowLaneClient)) {
|
|
458
583
|
slowLaneClient.destroy();
|
|
459
584
|
}
|
|
585
|
+
ingestedContextTexts.clear();
|
|
460
586
|
setDefaultSlowLaneClient(null);
|
|
461
587
|
}
|
|
462
588
|
})
|
|
@@ -109,13 +109,37 @@ export const BE_PARITY = {
|
|
|
109
109
|
*/
|
|
110
110
|
MAX_CONTEXT_WORDS: 100
|
|
111
111
|
};
|
|
112
|
+
const splitOnWhitespace = text => {
|
|
113
|
+
const trimmed = text.trim();
|
|
114
|
+
if (trimmed === '') {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
const words = [];
|
|
118
|
+
let wordStart = -1;
|
|
119
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
120
|
+
if (trimmed[i].trim() === '') {
|
|
121
|
+
if (wordStart !== -1) {
|
|
122
|
+
words.push(trimmed.slice(wordStart, i));
|
|
123
|
+
wordStart = -1;
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (wordStart === -1) {
|
|
128
|
+
wordStart = i;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (wordStart !== -1) {
|
|
132
|
+
words.push(trimmed.slice(wordStart));
|
|
133
|
+
}
|
|
134
|
+
return words;
|
|
135
|
+
};
|
|
112
136
|
|
|
113
137
|
/**
|
|
114
138
|
* Return the last `n` whitespace-separated words of `text`, joined by spaces.
|
|
115
139
|
* Mirrors the BE rolling-window truncation applied before both encoders.
|
|
116
140
|
*/
|
|
117
141
|
const truncateToLastNWords = (text, n) => {
|
|
118
|
-
const words = text
|
|
142
|
+
const words = splitOnWhitespace(text);
|
|
119
143
|
return words.length <= n ? text : words.slice(-n).join(' ');
|
|
120
144
|
};
|
|
121
145
|
|
|
@@ -457,6 +481,14 @@ export const createLocalSlowLaneClient = (config = {}) => {
|
|
|
457
481
|
let lastRequestedText = '';
|
|
458
482
|
let requestCounter = 0;
|
|
459
483
|
let latestRequestId = -1;
|
|
484
|
+
let inferenceInFlight = false;
|
|
485
|
+
let activeInferenceText = null;
|
|
486
|
+
// The requestId of the inference currently in flight. Tracked so the
|
|
487
|
+
// in-flight dedup path can restore `latestRequestId` to it — otherwise an
|
|
488
|
+
// intermediate keystroke that bumped `latestRequestId` would cause the
|
|
489
|
+
// in-flight (still-current) result to be discarded as stale.
|
|
490
|
+
let activeInferenceRequestId = -1;
|
|
491
|
+
let pendingInference = null;
|
|
460
492
|
let ready = false;
|
|
461
493
|
let destroyed = false;
|
|
462
494
|
let initFailed = false;
|
|
@@ -600,32 +632,34 @@ export const createLocalSlowLaneClient = (config = {}) => {
|
|
|
600
632
|
const lmText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_TOKENS);
|
|
601
633
|
const semanticText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_WORDS);
|
|
602
634
|
const arcticInput = wrapForArctic(semanticText);
|
|
635
|
+
const captureCompletionTime = (promise, onResolved) => promise.then(value => {
|
|
636
|
+
onResolved(performance.now());
|
|
637
|
+
return value;
|
|
638
|
+
});
|
|
603
639
|
if (isAutocompleteDebugEnabled()) {
|
|
604
640
|
// eslint-disable-next-line no-console
|
|
605
|
-
console.log(`%c[LocalSlowLane] %c🔢 Arctic input (${arcticInput.length} chars, ${semanticText
|
|
641
|
+
console.log(`%c[LocalSlowLane] %c🔢 Arctic input (${arcticInput.length} chars, ${splitOnWhitespace(semanticText).length} words): "${arcticInput.length > 100 ? `${arcticInput.slice(0, 100)}…` : arcticInput}"`, 'color: #9c27b0; font-weight: bold;', 'color: #009688;');
|
|
606
642
|
// eslint-disable-next-line no-console
|
|
607
|
-
console.log(`%c[LocalSlowLane] %c🧠 LM input (${lmText.length} chars, ${lmText
|
|
643
|
+
console.log(`%c[LocalSlowLane] %c🧠 LM input (${lmText.length} chars, ${splitOnWhitespace(lmText).length} words): "${lmText.length > 100 ? `${lmText.slice(0, 100)}…` : lmText}"`, 'color: #9c27b0; font-weight: bold;', 'color: #2196f3;');
|
|
608
644
|
}
|
|
609
645
|
try {
|
|
610
|
-
var
|
|
646
|
+
var _data, _data$;
|
|
611
647
|
const tStart = performance.now();
|
|
612
648
|
let tLmDone = 0;
|
|
613
649
|
let tEmbDone = 0;
|
|
614
|
-
const [, embeddingResponse] = await Promise.all([engine.completions.create({
|
|
650
|
+
const [, embeddingResponse] = await Promise.all([captureCompletionTime(engine.completions.create({
|
|
615
651
|
model: modelId,
|
|
616
652
|
prompt: lmText,
|
|
617
653
|
max_tokens: 1,
|
|
618
654
|
temperature: 0,
|
|
619
655
|
logprobs: false
|
|
620
|
-
})
|
|
621
|
-
tLmDone =
|
|
622
|
-
|
|
623
|
-
}), engine.embeddings.create({
|
|
656
|
+
}), resolvedAt => {
|
|
657
|
+
tLmDone = resolvedAt;
|
|
658
|
+
}), captureCompletionTime(engine.embeddings.create({
|
|
624
659
|
model: LOCAL_MLC_EMBEDDING_MODEL_ID,
|
|
625
660
|
input: arcticInput
|
|
626
|
-
})
|
|
627
|
-
tEmbDone =
|
|
628
|
-
return r;
|
|
661
|
+
}), resolvedAt => {
|
|
662
|
+
tEmbDone = resolvedAt;
|
|
629
663
|
})]);
|
|
630
664
|
if (isAutocompleteDebugEnabled()) {
|
|
631
665
|
// eslint-disable-next-line no-console
|
|
@@ -650,7 +684,7 @@ export const createLocalSlowLaneClient = (config = {}) => {
|
|
|
650
684
|
// Guard against base64-encoded responses (encoding_format: 'base64' would
|
|
651
685
|
// yield a string, and new Float32Array(string) silently produces an empty
|
|
652
686
|
// array, corrupting downstream cosine-similarity scoring).
|
|
653
|
-
const embedding = (
|
|
687
|
+
const embedding = (_data = embeddingResponse.data) === null || _data === void 0 ? void 0 : (_data$ = _data[0]) === null || _data$ === void 0 ? void 0 : _data$.embedding;
|
|
654
688
|
storedContextVector = Array.isArray(embedding) && embedding.length > 0 ? new Float32Array(embedding) : null;
|
|
655
689
|
if (isAutocompleteDebugEnabled()) {
|
|
656
690
|
// eslint-disable-next-line no-console
|
|
@@ -682,8 +716,8 @@ export const createLocalSlowLaneClient = (config = {}) => {
|
|
|
682
716
|
hasLmLogits: storedLmLogits !== null
|
|
683
717
|
});
|
|
684
718
|
} catch (err) {
|
|
685
|
-
// Discard errors for stale requests
|
|
686
|
-
if (requestId < latestRequestId) {
|
|
719
|
+
// Discard errors for stale requests or after teardown
|
|
720
|
+
if (requestId < latestRequestId || destroyed) {
|
|
687
721
|
return;
|
|
688
722
|
}
|
|
689
723
|
storedContextVector = null;
|
|
@@ -703,10 +737,46 @@ export const createLocalSlowLaneClient = (config = {}) => {
|
|
|
703
737
|
|
|
704
738
|
// ── Context update (debounced) ─────────────────────────────────────────
|
|
705
739
|
|
|
740
|
+
const startInference = (text, requestId) => {
|
|
741
|
+
// Self-contained guard: never start a new inference cycle after teardown,
|
|
742
|
+
// regardless of caller discipline.
|
|
743
|
+
if (destroyed) {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
inferenceInFlight = true;
|
|
747
|
+
activeInferenceText = text;
|
|
748
|
+
activeInferenceRequestId = requestId;
|
|
749
|
+
void ensureEngineInitialized().then(() => runInference(text, requestId)).catch(() => {}).finally(() => {
|
|
750
|
+
inferenceInFlight = false;
|
|
751
|
+
activeInferenceText = null;
|
|
752
|
+
activeInferenceRequestId = -1;
|
|
753
|
+
const next = pendingInference;
|
|
754
|
+
pendingInference = null;
|
|
755
|
+
if (next && !destroyed) {
|
|
756
|
+
startInference(next.text, next.requestId);
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
};
|
|
706
760
|
const doUpdateContext = text => {
|
|
761
|
+
var _pendingInference;
|
|
707
762
|
if (destroyed || !text || text.trim().length === 0) {
|
|
708
763
|
return;
|
|
709
764
|
}
|
|
765
|
+
if (inferenceInFlight && text === activeInferenceText) {
|
|
766
|
+
// The latest desired text already matches the in-flight inference, so
|
|
767
|
+
// re-running it would be wasted work. But an intermediate keystroke may
|
|
768
|
+
// have bumped `latestRequestId` past the in-flight request (and then been
|
|
769
|
+
// coalesced away), which would cause runInference to discard the
|
|
770
|
+
// still-current result as stale. Pin `latestRequestId` back to the active
|
|
771
|
+
// request so its result is accepted, and drop any now-superseded pending
|
|
772
|
+
// request.
|
|
773
|
+
latestRequestId = activeInferenceRequestId;
|
|
774
|
+
pendingInference = null;
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (inferenceInFlight && ((_pendingInference = pendingInference) === null || _pendingInference === void 0 ? void 0 : _pendingInference.text) === text) {
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
710
780
|
const requestId = ++requestCounter;
|
|
711
781
|
latestRequestId = requestId;
|
|
712
782
|
if (isAutocompleteDebugEnabled()) {
|
|
@@ -720,12 +790,27 @@ export const createLocalSlowLaneClient = (config = {}) => {
|
|
|
720
790
|
// eslint-disable-next-line no-console
|
|
721
791
|
console.groupEnd();
|
|
722
792
|
}
|
|
723
|
-
|
|
793
|
+
if (inferenceInFlight) {
|
|
794
|
+
pendingInference = {
|
|
795
|
+
text,
|
|
796
|
+
requestId
|
|
797
|
+
};
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
startInference(text, requestId);
|
|
724
801
|
};
|
|
725
802
|
const updateContextDebounced = text => {
|
|
726
803
|
if (debounceTimer) {
|
|
727
804
|
clearTimeout(debounceTimer);
|
|
728
805
|
}
|
|
806
|
+
if (inferenceInFlight) {
|
|
807
|
+
pendingInference = null;
|
|
808
|
+
if (text === activeInferenceText) {
|
|
809
|
+
latestRequestId = activeInferenceRequestId;
|
|
810
|
+
lastRequestedText = text;
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
729
814
|
lastRequestedText = text;
|
|
730
815
|
debounceTimer = setTimeout(() => {
|
|
731
816
|
debounceTimer = null;
|
|
@@ -758,6 +843,10 @@ export const createLocalSlowLaneClient = (config = {}) => {
|
|
|
758
843
|
unloadEngine(engineToUnload);
|
|
759
844
|
}
|
|
760
845
|
engineInitPromise = null;
|
|
846
|
+
inferenceInFlight = false;
|
|
847
|
+
activeInferenceText = null;
|
|
848
|
+
activeInferenceRequestId = -1;
|
|
849
|
+
pendingInference = null;
|
|
761
850
|
storedContextVector = null;
|
|
762
851
|
storedLmLogits = null;
|
|
763
852
|
}
|