@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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # @atlaskit/editor-plugin-autocomplete
2
2
 
3
+ ## 3.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`b6fb60030b3b9`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/b6fb60030b3b9) -
8
+ Improve local autocomplete scheduling latency
9
+
10
+ ## 3.3.0
11
+
12
+ ### Minor Changes
13
+
14
+ - [`4c8cf60a51e1d`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/4c8cf60a51e1d) -
15
+ Fix local slow lane client declaration builds
16
+
3
17
  ## 3.2.0
4
18
 
5
19
  ### Minor Changes
@@ -6,20 +6,38 @@ Object.defineProperty(exports, "__esModule", {
6
6
  });
7
7
  exports.createAutocompletePlugin = exports.autocompletePluginKey = void 0;
8
8
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
+ var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
9
10
  var _analytics = require("@atlaskit/editor-common/analytics");
10
11
  var _monitoring = require("@atlaskit/editor-common/monitoring");
11
12
  var _safePlugin = require("@atlaskit/editor-common/safe-plugin");
12
13
  var _keymap = require("@atlaskit/editor-prosemirror/keymap");
13
14
  var _state = require("@atlaskit/editor-prosemirror/state");
14
15
  var _view = require("@atlaskit/editor-prosemirror/view");
16
+ var _debugMode = require("./debug-mode");
15
17
  var _ghostTextDecoration = require("./ghost-text-decoration");
16
18
  var _localSlowLaneClient = require("./local-slow-lane-client");
17
19
  var _slowLaneClient = require("./slow-lane-client");
18
20
  var _textPredictor = require("./text-predictor");
21
+ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
22
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
23
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
19
24
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
20
25
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
21
26
  var autocompletePluginKey = exports.autocompletePluginKey = new _state.PluginKey('autocomplete');
22
27
  var DEBOUNCE_MS = 150;
28
+ var NETWORK_SLOW_LANE_DEBOUNCE_MS = 300;
29
+ var LOCAL_SLOW_LANE_DEBOUNCE_MS = 100;
30
+ var CONTEXT_REFRESH_THROTTLE_MS = 1000;
31
+ // Caps the dedup Set so long editing sessions don't retain every distinct
32
+ // version of the (potentially hundreds-of-KB) page content for the plugin's
33
+ // lifetime. Eviction is FIFO; a re-ingest of an evicted text is harmless.
34
+ var MAX_INGESTED_CONTEXT_TEXTS = 50;
35
+ // Caps how many times the word-boundary path will retry getContext() while the
36
+ // parent comment is still missing. Combined with the 1s throttle this gives a
37
+ // ~5s window to cover a still-loading comment thread, then stops permanently so
38
+ // non-comment editors (where parentCommentContent never arrives) don't refetch
39
+ // on every word boundary for the plugin's lifetime.
40
+ var MAX_CONTEXT_REFRESH_ATTEMPTS = 5;
23
41
  var hasDestroy = function hasDestroy(client) {
24
42
  return 'destroy' in client && typeof client.destroy === 'function';
25
43
  };
@@ -184,6 +202,12 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
184
202
  var debounceTimer = null;
185
203
  var hasIngestedPage = false;
186
204
  var resolvedContext;
205
+ /**
206
+ * Kept in sync with the live EditorView so the async getContext() promise
207
+ * can re-trigger a slow-lane update the moment context arrives, even if the
208
+ * user has already typed several words before the promise resolved.
209
+ */
210
+ var currentView = null;
187
211
  /**
188
212
  * Set after accepting a suggestion so the next doc-change update
189
213
  * skips scheduling a new prediction for the just-inserted text.
@@ -227,12 +251,125 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
227
251
  });
228
252
  };
229
253
  var slowLaneClient = options !== null && options !== void 0 && options.useLocalModel ? (0, _localSlowLaneClient.createLocalSlowLaneClient)({
230
- debounceMs: 300
254
+ debounceMs: LOCAL_SLOW_LANE_DEBOUNCE_MS
231
255
  }) : (0, _slowLaneClient.createSlowLaneClient)({
232
256
  baseUrl: '',
233
- debounceMs: 300
257
+ debounceMs: NETWORK_SLOW_LANE_DEBOUNCE_MS
234
258
  });
235
259
  (0, _slowLaneClient.setDefaultSlowLaneClient)(slowLaneClient);
260
+ var contextRequestInFlight = false;
261
+ var lastContextRefreshAt = 0;
262
+ // Bounds the word-boundary retry loop so it terminates even when the editor is
263
+ // not in a comment thread (parentCommentContent never resolves).
264
+ var wordBoundaryRefreshAttempts = 0;
265
+ // Set when the plugin is torn down so in-flight getContext() resolutions don't
266
+ // mutate the global text-predictor state after destruction.
267
+ var destroyed = false;
268
+ var ingestedContextTexts = new Set();
269
+ var logContextResolved = function logContextResolved(source, context) {
270
+ var _context$parentCommen, _context$siblingComme4, _context$siblingComme5;
271
+ if (!(0, _debugMode.isAutocompleteDebugEnabled)()) {
272
+ return;
273
+ }
274
+
275
+ // eslint-disable-next-line no-console
276
+ console.log('%c[Autocomplete] %cgetContext resolved', 'color: #00b8d9; font-weight: bold;', 'color: inherit;', {
277
+ source: source,
278
+ hasParentComment: !!(context !== null && context !== void 0 && context.parentCommentContent),
279
+ parentCommentPreview: context === null || context === void 0 || (_context$parentCommen = context.parentCommentContent) === null || _context$parentCommen === void 0 ? void 0 : _context$parentCommen.slice(0, 80),
280
+ siblingCount: (_context$siblingComme4 = context === null || context === void 0 || (_context$siblingComme5 = context.siblingCommentsContents) === null || _context$siblingComme5 === void 0 ? void 0 : _context$siblingComme5.length) !== null && _context$siblingComme4 !== void 0 ? _context$siblingComme4 : 0,
281
+ hasFullPage: !!(context !== null && context !== void 0 && context.fullPageContent)
282
+ });
283
+ };
284
+ var applyContext = function applyContext(context) {
285
+ var _context$siblingComme6;
286
+ // Merge rather than replace: the word-boundary retry may resolve only a
287
+ // late-arriving field (e.g. parentCommentContent) without re-sending
288
+ // fullPageContent, so replacing would drop previously resolved context.
289
+ // Strip undefined values first so a field explicitly set to undefined by
290
+ // getContext doesn't overwrite a previously resolved value.
291
+ var definedContext = Object.fromEntries(Object.entries(context).filter(function (_ref) {
292
+ var _ref2 = (0, _slicedToArray2.default)(_ref, 2),
293
+ value = _ref2[1];
294
+ return value !== undefined;
295
+ }));
296
+ resolvedContext = _objectSpread(_objectSpread({}, resolvedContext), definedContext);
297
+ var ingestContextText = function ingestContextText(text) {
298
+ if (!text || ingestedContextTexts.has(text)) {
299
+ return;
300
+ }
301
+ ingestedContextTexts.add(text);
302
+ // Evict oldest entries (Set preserves insertion order) to bound memory.
303
+ while (ingestedContextTexts.size > MAX_INGESTED_CONTEXT_TEXTS) {
304
+ var oldest = ingestedContextTexts.values().next().value;
305
+ if (oldest === undefined) {
306
+ break;
307
+ }
308
+ ingestedContextTexts.delete(oldest);
309
+ }
310
+ (0, _textPredictor.ingestDocumentPage)(text);
311
+ };
312
+ ingestContextText(context.fullPageContent);
313
+ ingestContextText(context.parentCommentContent);
314
+ var _iterator = _createForOfIteratorHelper((_context$siblingComme6 = context.siblingCommentsContents) !== null && _context$siblingComme6 !== void 0 ? _context$siblingComme6 : []),
315
+ _step;
316
+ try {
317
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
318
+ var siblingCommentContent = _step.value;
319
+ ingestContextText(siblingCommentContent);
320
+ }
321
+
322
+ // Context arrived after word boundaries may already have fired. Re-send
323
+ // slow-lane context immediately so the next inference includes the thread.
324
+ } catch (err) {
325
+ _iterator.e(err);
326
+ } finally {
327
+ _iterator.f();
328
+ }
329
+ if (currentView) {
330
+ slowLaneClient.updateContext(buildSlowLaneText(currentView.state.doc.textContent, resolvedContext));
331
+ }
332
+ };
333
+
334
+ /**
335
+ * Returns true when a fetch was actually started, false when it was skipped
336
+ * (no getContext, a request already in flight, or throttled). Callers that
337
+ * track a retry budget should only count attempts where this returns true.
338
+ */
339
+ var refreshContext = function refreshContext(_ref3) {
340
+ var source = _ref3.source,
341
+ _ref3$allowThrottle = _ref3.allowThrottle,
342
+ allowThrottle = _ref3$allowThrottle === void 0 ? true : _ref3$allowThrottle;
343
+ if (!(options !== null && options !== void 0 && options.getContext) || contextRequestInFlight) {
344
+ return false;
345
+ }
346
+ var now = Date.now();
347
+ if (allowThrottle && now - lastContextRefreshAt < CONTEXT_REFRESH_THROTTLE_MS) {
348
+ return false;
349
+ }
350
+ contextRequestInFlight = true;
351
+ lastContextRefreshAt = now;
352
+ options.getContext().then(function (context) {
353
+ // Bail if the plugin was destroyed while the fetch was in flight —
354
+ // applyContext mutates global text-predictor state we must not touch
355
+ // after teardown.
356
+ if (destroyed) {
357
+ return;
358
+ }
359
+ logContextResolved(source, context);
360
+ if (!context) {
361
+ return;
362
+ }
363
+ applyContext(context);
364
+ }).catch(function (error) {
365
+ (0, _monitoring.logException)(error, {
366
+ location: 'editor-plugin-autocomplete/getContext'
367
+ });
368
+ }).finally(function () {
369
+ contextRequestInFlight = false;
370
+ });
371
+ return true;
372
+ };
236
373
 
237
374
  /**
238
375
  * Schedule a prediction after a short debounce.
@@ -404,26 +541,10 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
404
541
  });
405
542
  if (!hasIngestedPage) {
406
543
  hasIngestedPage = true;
407
- if (options !== null && options !== void 0 && options.getContext) {
408
- options.getContext().then(function (context) {
409
- var _context$siblingComme4;
410
- if (!context) {
411
- return;
412
- }
413
- resolvedContext = context;
414
- if (context.fullPageContent) {
415
- (0, _textPredictor.ingestDocumentPage)(context.fullPageContent);
416
- }
417
- if (context.parentCommentContent) {
418
- (0, _textPredictor.ingestDocumentPage)(context.parentCommentContent);
419
- }
420
- (_context$siblingComme4 = context.siblingCommentsContents) === null || _context$siblingComme4 === void 0 || _context$siblingComme4.forEach(_textPredictor.ingestDocumentPage);
421
- }).catch(function (error) {
422
- (0, _monitoring.logException)(error, {
423
- location: 'editor-plugin-autocomplete/getContext'
424
- });
425
- });
426
- }
544
+ refreshContext({
545
+ source: 'focus',
546
+ allowThrottle: false
547
+ });
427
548
  }
428
549
  return false;
429
550
  }
@@ -432,6 +553,7 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
432
553
  view: function view() {
433
554
  return {
434
555
  update: function update(view, prevState) {
556
+ currentView = view;
435
557
  if (!prevState.doc.eq(view.state.doc)) {
436
558
  if (justAccepted) {
437
559
  justAccepted = false;
@@ -447,18 +569,36 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
447
569
  maybeUpdateSessionFrequency(view, prevState);
448
570
  var textBefore = getTextBeforeCursor(view.state);
449
571
  if ((0, _slowLaneClient.isWordBoundary)(textBefore)) {
572
+ var _resolvedContext;
450
573
  slowLaneClient.updateContext(buildSlowLaneText(view.state.doc.textContent, resolvedContext));
574
+
575
+ // Context may not have resolved on first focus (e.g. comment
576
+ // thread still loading). Retry on word boundaries until we have
577
+ // the parent comment, throttled so we don't refetch constantly
578
+ // and capped so non-comment editors stop retrying entirely.
579
+ if (!((_resolvedContext = resolvedContext) !== null && _resolvedContext !== void 0 && _resolvedContext.parentCommentContent) && wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS) {
580
+ // Only count the attempt when a fetch actually started, so an
581
+ // in-flight or throttled no-op doesn't burn the retry budget.
582
+ if (refreshContext({
583
+ source: 'word-boundary'
584
+ })) {
585
+ wordBoundaryRefreshAttempts++;
586
+ }
587
+ }
451
588
  }
452
589
  schedulePrediction(view);
453
590
  }
454
591
  },
455
592
  destroy: function destroy() {
593
+ destroyed = true;
594
+ currentView = null;
456
595
  if (debounceTimer) {
457
596
  clearTimeout(debounceTimer);
458
597
  }
459
598
  if (hasDestroy(slowLaneClient)) {
460
599
  slowLaneClient.destroy();
461
600
  }
601
+ ingestedContextTexts.clear();
462
602
  (0, _slowLaneClient.setDefaultSlowLaneClient)(null);
463
603
  }
464
604
  };
@@ -129,13 +129,37 @@ var BE_PARITY = exports.BE_PARITY = {
129
129
  */
130
130
  MAX_CONTEXT_WORDS: 100
131
131
  };
132
+ var splitOnWhitespace = function splitOnWhitespace(text) {
133
+ var trimmed = text.trim();
134
+ if (trimmed === '') {
135
+ return [];
136
+ }
137
+ var words = [];
138
+ var wordStart = -1;
139
+ for (var i = 0; i < trimmed.length; i++) {
140
+ if (trimmed[i].trim() === '') {
141
+ if (wordStart !== -1) {
142
+ words.push(trimmed.slice(wordStart, i));
143
+ wordStart = -1;
144
+ }
145
+ continue;
146
+ }
147
+ if (wordStart === -1) {
148
+ wordStart = i;
149
+ }
150
+ }
151
+ if (wordStart !== -1) {
152
+ words.push(trimmed.slice(wordStart));
153
+ }
154
+ return words;
155
+ };
132
156
 
133
157
  /**
134
158
  * Return the last `n` whitespace-separated words of `text`, joined by spaces.
135
159
  * Mirrors the BE rolling-window truncation applied before both encoders.
136
160
  */
137
161
  var truncateToLastNWords = function truncateToLastNWords(text, n) {
138
- var words = text.trim().split(/[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]+/);
162
+ var words = splitOnWhitespace(text);
139
163
  return words.length <= n ? text : words.slice(-n).join(' ');
140
164
  };
141
165
 
@@ -598,6 +622,14 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
598
622
  var lastRequestedText = '';
599
623
  var requestCounter = 0;
600
624
  var latestRequestId = -1;
625
+ var inferenceInFlight = false;
626
+ var activeInferenceText = null;
627
+ // The requestId of the inference currently in flight. Tracked so the
628
+ // in-flight dedup path can restore `latestRequestId` to it — otherwise an
629
+ // intermediate keystroke that bumped `latestRequestId` would cause the
630
+ // in-flight (still-current) result to be discarded as stale.
631
+ var activeInferenceRequestId = -1;
632
+ var pendingInference = null;
601
633
  var ready = false;
602
634
  var destroyed = false;
603
635
  var initFailed = false;
@@ -749,7 +781,7 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
749
781
  */
750
782
  var runInference = /*#__PURE__*/function () {
751
783
  var _ref9 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3(text, requestId) {
752
- var lmText, semanticText, arcticInput, _embeddingResponse$da, tStart, tLmDone, tEmbDone, _yield$Promise$all5, _yield$Promise$all6, embeddingResponse, rawLogits, payload, embedding, sumSq, i, topTokens, errorMsg, _t2;
784
+ var lmText, semanticText, arcticInput, captureCompletionTime, _data, tStart, tLmDone, tEmbDone, _yield$Promise$all5, _yield$Promise$all6, embeddingResponse, rawLogits, payload, embedding, sumSq, i, topTokens, errorMsg, _t2;
753
785
  return _regenerator.default.wrap(function (_context3) {
754
786
  while (1) switch (_context3.prev = _context3.next) {
755
787
  case 0:
@@ -772,32 +804,36 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
772
804
  lmText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_TOKENS);
773
805
  semanticText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_WORDS);
774
806
  arcticInput = wrapForArctic(semanticText);
807
+ captureCompletionTime = function captureCompletionTime(promise, onResolved) {
808
+ return promise.then(function (value) {
809
+ onResolved(performance.now());
810
+ return value;
811
+ });
812
+ };
775
813
  if ((0, _debugMode.isAutocompleteDebugEnabled)()) {
776
814
  // eslint-disable-next-line no-console
777
- console.log("%c[LocalSlowLane] %c\uD83D\uDD22 Arctic input (".concat(arcticInput.length, " chars, ").concat(semanticText.split(/[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]+/).length, " words): \"").concat(arcticInput.length > 100 ? "".concat(arcticInput.slice(0, 100), "\u2026") : arcticInput, "\""), 'color: #9c27b0; font-weight: bold;', 'color: #009688;');
815
+ console.log("%c[LocalSlowLane] %c\uD83D\uDD22 Arctic input (".concat(arcticInput.length, " chars, ").concat(splitOnWhitespace(semanticText).length, " words): \"").concat(arcticInput.length > 100 ? "".concat(arcticInput.slice(0, 100), "\u2026") : arcticInput, "\""), 'color: #9c27b0; font-weight: bold;', 'color: #009688;');
778
816
  // eslint-disable-next-line no-console
779
- console.log("%c[LocalSlowLane] %c\uD83E\uDDE0 LM input (".concat(lmText.length, " chars, ").concat(lmText.split(/[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]+/).length, " words): \"").concat(lmText.length > 100 ? "".concat(lmText.slice(0, 100), "\u2026") : lmText, "\""), 'color: #9c27b0; font-weight: bold;', 'color: #2196f3;');
817
+ console.log("%c[LocalSlowLane] %c\uD83E\uDDE0 LM input (".concat(lmText.length, " chars, ").concat(splitOnWhitespace(lmText).length, " words): \"").concat(lmText.length > 100 ? "".concat(lmText.slice(0, 100), "\u2026") : lmText, "\""), 'color: #9c27b0; font-weight: bold;', 'color: #2196f3;');
780
818
  }
781
819
  _context3.prev = 2;
782
820
  tStart = performance.now();
783
821
  tLmDone = 0;
784
822
  tEmbDone = 0;
785
823
  _context3.next = 3;
786
- return Promise.all([engine.completions.create({
824
+ return Promise.all([captureCompletionTime(engine.completions.create({
787
825
  model: modelId,
788
826
  prompt: lmText,
789
827
  max_tokens: 1,
790
828
  temperature: 0,
791
829
  logprobs: false
792
- }).then(function (r) {
793
- tLmDone = performance.now();
794
- return r;
795
- }), engine.embeddings.create({
830
+ }), function (resolvedAt) {
831
+ tLmDone = resolvedAt;
832
+ }), captureCompletionTime(engine.embeddings.create({
796
833
  model: LOCAL_MLC_EMBEDDING_MODEL_ID,
797
834
  input: arcticInput
798
- }).then(function (r) {
799
- tEmbDone = performance.now();
800
- return r;
835
+ }), function (resolvedAt) {
836
+ tEmbDone = resolvedAt;
801
837
  })]);
802
838
  case 3:
803
839
  _yield$Promise$all5 = _context3.sent;
@@ -828,7 +864,7 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
828
864
  // Guard against base64-encoded responses (encoding_format: 'base64' would
829
865
  // yield a string, and new Float32Array(string) silently produces an empty
830
866
  // array, corrupting downstream cosine-similarity scoring).
831
- embedding = (_embeddingResponse$da = embeddingResponse.data) === null || _embeddingResponse$da === void 0 || (_embeddingResponse$da = _embeddingResponse$da[0]) === null || _embeddingResponse$da === void 0 ? void 0 : _embeddingResponse$da.embedding;
867
+ embedding = (_data = embeddingResponse.data) === null || _data === void 0 || (_data = _data[0]) === null || _data === void 0 ? void 0 : _data.embedding;
832
868
  storedContextVector = Array.isArray(embedding) && embedding.length > 0 ? new Float32Array(embedding) : null;
833
869
  if ((0, _debugMode.isAutocompleteDebugEnabled)()) {
834
870
  // eslint-disable-next-line no-console
@@ -874,7 +910,7 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
874
910
  case 5:
875
911
  _context3.prev = 5;
876
912
  _t2 = _context3["catch"](2);
877
- if (!(requestId < latestRequestId)) {
913
+ if (!(requestId < latestRequestId || destroyed)) {
878
914
  _context3.next = 6;
879
915
  break;
880
916
  }
@@ -905,10 +941,48 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
905
941
 
906
942
  // ── Context update (debounced) ─────────────────────────────────────────
907
943
 
944
+ var _startInference = function startInference(text, requestId) {
945
+ // Self-contained guard: never start a new inference cycle after teardown,
946
+ // regardless of caller discipline.
947
+ if (destroyed) {
948
+ return;
949
+ }
950
+ inferenceInFlight = true;
951
+ activeInferenceText = text;
952
+ activeInferenceRequestId = requestId;
953
+ void ensureEngineInitialized().then(function () {
954
+ return runInference(text, requestId);
955
+ }).catch(function () {}).finally(function () {
956
+ inferenceInFlight = false;
957
+ activeInferenceText = null;
958
+ activeInferenceRequestId = -1;
959
+ var next = pendingInference;
960
+ pendingInference = null;
961
+ if (next && !destroyed) {
962
+ _startInference(next.text, next.requestId);
963
+ }
964
+ });
965
+ };
908
966
  var doUpdateContext = function doUpdateContext(text) {
967
+ var _pendingInference;
909
968
  if (destroyed || !text || text.trim().length === 0) {
910
969
  return;
911
970
  }
971
+ if (inferenceInFlight && text === activeInferenceText) {
972
+ // The latest desired text already matches the in-flight inference, so
973
+ // re-running it would be wasted work. But an intermediate keystroke may
974
+ // have bumped `latestRequestId` past the in-flight request (and then been
975
+ // coalesced away), which would cause runInference to discard the
976
+ // still-current result as stale. Pin `latestRequestId` back to the active
977
+ // request so its result is accepted, and drop any now-superseded pending
978
+ // request.
979
+ latestRequestId = activeInferenceRequestId;
980
+ pendingInference = null;
981
+ return;
982
+ }
983
+ if (inferenceInFlight && ((_pendingInference = pendingInference) === null || _pendingInference === void 0 ? void 0 : _pendingInference.text) === text) {
984
+ return;
985
+ }
912
986
  var requestId = ++requestCounter;
913
987
  latestRequestId = requestId;
914
988
  if ((0, _debugMode.isAutocompleteDebugEnabled)()) {
@@ -922,14 +996,27 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
922
996
  // eslint-disable-next-line no-console
923
997
  console.groupEnd();
924
998
  }
925
- void ensureEngineInitialized().then(function () {
926
- return runInference(text, requestId);
927
- }).catch(function () {});
999
+ if (inferenceInFlight) {
1000
+ pendingInference = {
1001
+ text: text,
1002
+ requestId: requestId
1003
+ };
1004
+ return;
1005
+ }
1006
+ _startInference(text, requestId);
928
1007
  };
929
1008
  var updateContextDebounced = function updateContextDebounced(text) {
930
1009
  if (debounceTimer) {
931
1010
  clearTimeout(debounceTimer);
932
1011
  }
1012
+ if (inferenceInFlight) {
1013
+ pendingInference = null;
1014
+ if (text === activeInferenceText) {
1015
+ latestRequestId = activeInferenceRequestId;
1016
+ lastRequestedText = text;
1017
+ return;
1018
+ }
1019
+ }
933
1020
  lastRequestedText = text;
934
1021
  debounceTimer = setTimeout(function () {
935
1022
  debounceTimer = null;
@@ -968,6 +1055,10 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
968
1055
  unloadEngine(engineToUnload);
969
1056
  }
970
1057
  engineInitPromise = null;
1058
+ inferenceInFlight = false;
1059
+ activeInferenceText = null;
1060
+ activeInferenceRequestId = -1;
1061
+ pendingInference = null;
971
1062
  storedContextVector = null;
972
1063
  storedLmLogits = null;
973
1064
  }