@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.
@@ -1,4 +1,8 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
3
+ 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; } } }; }
4
+ 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; } }
5
+ 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; }
2
6
  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; }
3
7
  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) { _defineProperty(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; }
4
8
  import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
@@ -7,12 +11,26 @@ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
7
11
  import { keydownHandler } from '@atlaskit/editor-prosemirror/keymap';
8
12
  import { PluginKey } from '@atlaskit/editor-prosemirror/state';
9
13
  import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
14
+ import { isAutocompleteDebugEnabled } from './debug-mode';
10
15
  import { createGhostTextDecorationSet } from './ghost-text-decoration';
11
16
  import { createLocalSlowLaneClient } from './local-slow-lane-client';
12
17
  import { createSlowLaneClient, setDefaultSlowLaneClient, isWordBoundary } from './slow-lane-client';
13
18
  import { predict, loadDefaultVocabulary, loadVectorsAsync, incrementSessionFreq, ingestDocumentPage } from './text-predictor';
14
19
  export var autocompletePluginKey = new PluginKey('autocomplete');
15
20
  var DEBOUNCE_MS = 150;
21
+ var NETWORK_SLOW_LANE_DEBOUNCE_MS = 300;
22
+ var LOCAL_SLOW_LANE_DEBOUNCE_MS = 100;
23
+ var CONTEXT_REFRESH_THROTTLE_MS = 1000;
24
+ // Caps the dedup Set so long editing sessions don't retain every distinct
25
+ // version of the (potentially hundreds-of-KB) page content for the plugin's
26
+ // lifetime. Eviction is FIFO; a re-ingest of an evicted text is harmless.
27
+ var MAX_INGESTED_CONTEXT_TEXTS = 50;
28
+ // Caps how many times the word-boundary path will retry getContext() while the
29
+ // parent comment is still missing. Combined with the 1s throttle this gives a
30
+ // ~5s window to cover a still-loading comment thread, then stops permanently so
31
+ // non-comment editors (where parentCommentContent never arrives) don't refetch
32
+ // on every word boundary for the plugin's lifetime.
33
+ var MAX_CONTEXT_REFRESH_ATTEMPTS = 5;
16
34
  var hasDestroy = function hasDestroy(client) {
17
35
  return 'destroy' in client && typeof client.destroy === 'function';
18
36
  };
@@ -177,6 +195,12 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
177
195
  var debounceTimer = null;
178
196
  var hasIngestedPage = false;
179
197
  var resolvedContext;
198
+ /**
199
+ * Kept in sync with the live EditorView so the async getContext() promise
200
+ * can re-trigger a slow-lane update the moment context arrives, even if the
201
+ * user has already typed several words before the promise resolved.
202
+ */
203
+ var currentView = null;
180
204
  /**
181
205
  * Set after accepting a suggestion so the next doc-change update
182
206
  * skips scheduling a new prediction for the just-inserted text.
@@ -220,12 +244,125 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
220
244
  });
221
245
  };
222
246
  var slowLaneClient = options !== null && options !== void 0 && options.useLocalModel ? createLocalSlowLaneClient({
223
- debounceMs: 300
247
+ debounceMs: LOCAL_SLOW_LANE_DEBOUNCE_MS
224
248
  }) : createSlowLaneClient({
225
249
  baseUrl: '',
226
- debounceMs: 300
250
+ debounceMs: NETWORK_SLOW_LANE_DEBOUNCE_MS
227
251
  });
228
252
  setDefaultSlowLaneClient(slowLaneClient);
253
+ var contextRequestInFlight = false;
254
+ var lastContextRefreshAt = 0;
255
+ // Bounds the word-boundary retry loop so it terminates even when the editor is
256
+ // not in a comment thread (parentCommentContent never resolves).
257
+ var wordBoundaryRefreshAttempts = 0;
258
+ // Set when the plugin is torn down so in-flight getContext() resolutions don't
259
+ // mutate the global text-predictor state after destruction.
260
+ var destroyed = false;
261
+ var ingestedContextTexts = new Set();
262
+ var logContextResolved = function logContextResolved(source, context) {
263
+ var _context$parentCommen, _context$siblingComme4, _context$siblingComme5;
264
+ if (!isAutocompleteDebugEnabled()) {
265
+ return;
266
+ }
267
+
268
+ // eslint-disable-next-line no-console
269
+ console.log('%c[Autocomplete] %cgetContext resolved', 'color: #00b8d9; font-weight: bold;', 'color: inherit;', {
270
+ source: source,
271
+ hasParentComment: !!(context !== null && context !== void 0 && context.parentCommentContent),
272
+ parentCommentPreview: context === null || context === void 0 || (_context$parentCommen = context.parentCommentContent) === null || _context$parentCommen === void 0 ? void 0 : _context$parentCommen.slice(0, 80),
273
+ 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,
274
+ hasFullPage: !!(context !== null && context !== void 0 && context.fullPageContent)
275
+ });
276
+ };
277
+ var applyContext = function applyContext(context) {
278
+ var _context$siblingComme6;
279
+ // Merge rather than replace: the word-boundary retry may resolve only a
280
+ // late-arriving field (e.g. parentCommentContent) without re-sending
281
+ // fullPageContent, so replacing would drop previously resolved context.
282
+ // Strip undefined values first so a field explicitly set to undefined by
283
+ // getContext doesn't overwrite a previously resolved value.
284
+ var definedContext = Object.fromEntries(Object.entries(context).filter(function (_ref) {
285
+ var _ref2 = _slicedToArray(_ref, 2),
286
+ value = _ref2[1];
287
+ return value !== undefined;
288
+ }));
289
+ resolvedContext = _objectSpread(_objectSpread({}, resolvedContext), definedContext);
290
+ var ingestContextText = function ingestContextText(text) {
291
+ if (!text || ingestedContextTexts.has(text)) {
292
+ return;
293
+ }
294
+ ingestedContextTexts.add(text);
295
+ // Evict oldest entries (Set preserves insertion order) to bound memory.
296
+ while (ingestedContextTexts.size > MAX_INGESTED_CONTEXT_TEXTS) {
297
+ var oldest = ingestedContextTexts.values().next().value;
298
+ if (oldest === undefined) {
299
+ break;
300
+ }
301
+ ingestedContextTexts.delete(oldest);
302
+ }
303
+ ingestDocumentPage(text);
304
+ };
305
+ ingestContextText(context.fullPageContent);
306
+ ingestContextText(context.parentCommentContent);
307
+ var _iterator = _createForOfIteratorHelper((_context$siblingComme6 = context.siblingCommentsContents) !== null && _context$siblingComme6 !== void 0 ? _context$siblingComme6 : []),
308
+ _step;
309
+ try {
310
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
311
+ var siblingCommentContent = _step.value;
312
+ ingestContextText(siblingCommentContent);
313
+ }
314
+
315
+ // Context arrived after word boundaries may already have fired. Re-send
316
+ // slow-lane context immediately so the next inference includes the thread.
317
+ } catch (err) {
318
+ _iterator.e(err);
319
+ } finally {
320
+ _iterator.f();
321
+ }
322
+ if (currentView) {
323
+ slowLaneClient.updateContext(buildSlowLaneText(currentView.state.doc.textContent, resolvedContext));
324
+ }
325
+ };
326
+
327
+ /**
328
+ * Returns true when a fetch was actually started, false when it was skipped
329
+ * (no getContext, a request already in flight, or throttled). Callers that
330
+ * track a retry budget should only count attempts where this returns true.
331
+ */
332
+ var refreshContext = function refreshContext(_ref3) {
333
+ var source = _ref3.source,
334
+ _ref3$allowThrottle = _ref3.allowThrottle,
335
+ allowThrottle = _ref3$allowThrottle === void 0 ? true : _ref3$allowThrottle;
336
+ if (!(options !== null && options !== void 0 && options.getContext) || contextRequestInFlight) {
337
+ return false;
338
+ }
339
+ var now = Date.now();
340
+ if (allowThrottle && now - lastContextRefreshAt < CONTEXT_REFRESH_THROTTLE_MS) {
341
+ return false;
342
+ }
343
+ contextRequestInFlight = true;
344
+ lastContextRefreshAt = now;
345
+ options.getContext().then(function (context) {
346
+ // Bail if the plugin was destroyed while the fetch was in flight —
347
+ // applyContext mutates global text-predictor state we must not touch
348
+ // after teardown.
349
+ if (destroyed) {
350
+ return;
351
+ }
352
+ logContextResolved(source, context);
353
+ if (!context) {
354
+ return;
355
+ }
356
+ applyContext(context);
357
+ }).catch(function (error) {
358
+ logException(error, {
359
+ location: 'editor-plugin-autocomplete/getContext'
360
+ });
361
+ }).finally(function () {
362
+ contextRequestInFlight = false;
363
+ });
364
+ return true;
365
+ };
229
366
 
230
367
  /**
231
368
  * Schedule a prediction after a short debounce.
@@ -397,26 +534,10 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
397
534
  });
398
535
  if (!hasIngestedPage) {
399
536
  hasIngestedPage = true;
400
- if (options !== null && options !== void 0 && options.getContext) {
401
- options.getContext().then(function (context) {
402
- var _context$siblingComme4;
403
- if (!context) {
404
- return;
405
- }
406
- resolvedContext = context;
407
- if (context.fullPageContent) {
408
- ingestDocumentPage(context.fullPageContent);
409
- }
410
- if (context.parentCommentContent) {
411
- ingestDocumentPage(context.parentCommentContent);
412
- }
413
- (_context$siblingComme4 = context.siblingCommentsContents) === null || _context$siblingComme4 === void 0 || _context$siblingComme4.forEach(ingestDocumentPage);
414
- }).catch(function (error) {
415
- logException(error, {
416
- location: 'editor-plugin-autocomplete/getContext'
417
- });
418
- });
419
- }
537
+ refreshContext({
538
+ source: 'focus',
539
+ allowThrottle: false
540
+ });
420
541
  }
421
542
  return false;
422
543
  }
@@ -425,6 +546,7 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
425
546
  view: function view() {
426
547
  return {
427
548
  update: function update(view, prevState) {
549
+ currentView = view;
428
550
  if (!prevState.doc.eq(view.state.doc)) {
429
551
  if (justAccepted) {
430
552
  justAccepted = false;
@@ -440,18 +562,36 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
440
562
  maybeUpdateSessionFrequency(view, prevState);
441
563
  var textBefore = getTextBeforeCursor(view.state);
442
564
  if (isWordBoundary(textBefore)) {
565
+ var _resolvedContext;
443
566
  slowLaneClient.updateContext(buildSlowLaneText(view.state.doc.textContent, resolvedContext));
567
+
568
+ // Context may not have resolved on first focus (e.g. comment
569
+ // thread still loading). Retry on word boundaries until we have
570
+ // the parent comment, throttled so we don't refetch constantly
571
+ // and capped so non-comment editors stop retrying entirely.
572
+ if (!((_resolvedContext = resolvedContext) !== null && _resolvedContext !== void 0 && _resolvedContext.parentCommentContent) && wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS) {
573
+ // Only count the attempt when a fetch actually started, so an
574
+ // in-flight or throttled no-op doesn't burn the retry budget.
575
+ if (refreshContext({
576
+ source: 'word-boundary'
577
+ })) {
578
+ wordBoundaryRefreshAttempts++;
579
+ }
580
+ }
444
581
  }
445
582
  schedulePrediction(view);
446
583
  }
447
584
  },
448
585
  destroy: function destroy() {
586
+ destroyed = true;
587
+ currentView = null;
449
588
  if (debounceTimer) {
450
589
  clearTimeout(debounceTimer);
451
590
  }
452
591
  if (hasDestroy(slowLaneClient)) {
453
592
  slowLaneClient.destroy();
454
593
  }
594
+ ingestedContextTexts.clear();
455
595
  setDefaultSlowLaneClient(null);
456
596
  }
457
597
  };
@@ -123,13 +123,37 @@ export var BE_PARITY = {
123
123
  */
124
124
  MAX_CONTEXT_WORDS: 100
125
125
  };
126
+ var splitOnWhitespace = function splitOnWhitespace(text) {
127
+ var trimmed = text.trim();
128
+ if (trimmed === '') {
129
+ return [];
130
+ }
131
+ var words = [];
132
+ var wordStart = -1;
133
+ for (var i = 0; i < trimmed.length; i++) {
134
+ if (trimmed[i].trim() === '') {
135
+ if (wordStart !== -1) {
136
+ words.push(trimmed.slice(wordStart, i));
137
+ wordStart = -1;
138
+ }
139
+ continue;
140
+ }
141
+ if (wordStart === -1) {
142
+ wordStart = i;
143
+ }
144
+ }
145
+ if (wordStart !== -1) {
146
+ words.push(trimmed.slice(wordStart));
147
+ }
148
+ return words;
149
+ };
126
150
 
127
151
  /**
128
152
  * Return the last `n` whitespace-separated words of `text`, joined by spaces.
129
153
  * Mirrors the BE rolling-window truncation applied before both encoders.
130
154
  */
131
155
  var truncateToLastNWords = function truncateToLastNWords(text, n) {
132
- var words = text.trim().split(/[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]+/);
156
+ var words = splitOnWhitespace(text);
133
157
  return words.length <= n ? text : words.slice(-n).join(' ');
134
158
  };
135
159
 
@@ -588,6 +612,14 @@ export var createLocalSlowLaneClient = function createLocalSlowLaneClient() {
588
612
  var lastRequestedText = '';
589
613
  var requestCounter = 0;
590
614
  var latestRequestId = -1;
615
+ var inferenceInFlight = false;
616
+ var activeInferenceText = null;
617
+ // The requestId of the inference currently in flight. Tracked so the
618
+ // in-flight dedup path can restore `latestRequestId` to it — otherwise an
619
+ // intermediate keystroke that bumped `latestRequestId` would cause the
620
+ // in-flight (still-current) result to be discarded as stale.
621
+ var activeInferenceRequestId = -1;
622
+ var pendingInference = null;
591
623
  var ready = false;
592
624
  var destroyed = false;
593
625
  var initFailed = false;
@@ -737,7 +769,7 @@ export var createLocalSlowLaneClient = function createLocalSlowLaneClient() {
737
769
  */
738
770
  var runInference = /*#__PURE__*/function () {
739
771
  var _ref9 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee3(text, requestId) {
740
- var lmText, semanticText, arcticInput, _embeddingResponse$da, tStart, tLmDone, tEmbDone, _yield$Promise$all5, _yield$Promise$all6, embeddingResponse, rawLogits, payload, embedding, sumSq, i, topTokens, errorMsg, _t2;
772
+ var lmText, semanticText, arcticInput, captureCompletionTime, _data, tStart, tLmDone, tEmbDone, _yield$Promise$all5, _yield$Promise$all6, embeddingResponse, rawLogits, payload, embedding, sumSq, i, topTokens, errorMsg, _t2;
741
773
  return _regeneratorRuntime.wrap(function (_context3) {
742
774
  while (1) switch (_context3.prev = _context3.next) {
743
775
  case 0:
@@ -760,32 +792,36 @@ export var createLocalSlowLaneClient = function createLocalSlowLaneClient() {
760
792
  lmText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_TOKENS);
761
793
  semanticText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_WORDS);
762
794
  arcticInput = wrapForArctic(semanticText);
795
+ captureCompletionTime = function captureCompletionTime(promise, onResolved) {
796
+ return promise.then(function (value) {
797
+ onResolved(performance.now());
798
+ return value;
799
+ });
800
+ };
763
801
  if (isAutocompleteDebugEnabled()) {
764
802
  // eslint-disable-next-line no-console
765
- 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;');
803
+ 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;');
766
804
  // eslint-disable-next-line no-console
767
- 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;');
805
+ 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;');
768
806
  }
769
807
  _context3.prev = 2;
770
808
  tStart = performance.now();
771
809
  tLmDone = 0;
772
810
  tEmbDone = 0;
773
811
  _context3.next = 3;
774
- return Promise.all([engine.completions.create({
812
+ return Promise.all([captureCompletionTime(engine.completions.create({
775
813
  model: modelId,
776
814
  prompt: lmText,
777
815
  max_tokens: 1,
778
816
  temperature: 0,
779
817
  logprobs: false
780
- }).then(function (r) {
781
- tLmDone = performance.now();
782
- return r;
783
- }), engine.embeddings.create({
818
+ }), function (resolvedAt) {
819
+ tLmDone = resolvedAt;
820
+ }), captureCompletionTime(engine.embeddings.create({
784
821
  model: LOCAL_MLC_EMBEDDING_MODEL_ID,
785
822
  input: arcticInput
786
- }).then(function (r) {
787
- tEmbDone = performance.now();
788
- return r;
823
+ }), function (resolvedAt) {
824
+ tEmbDone = resolvedAt;
789
825
  })]);
790
826
  case 3:
791
827
  _yield$Promise$all5 = _context3.sent;
@@ -816,7 +852,7 @@ export var createLocalSlowLaneClient = function createLocalSlowLaneClient() {
816
852
  // Guard against base64-encoded responses (encoding_format: 'base64' would
817
853
  // yield a string, and new Float32Array(string) silently produces an empty
818
854
  // array, corrupting downstream cosine-similarity scoring).
819
- 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;
855
+ embedding = (_data = embeddingResponse.data) === null || _data === void 0 || (_data = _data[0]) === null || _data === void 0 ? void 0 : _data.embedding;
820
856
  storedContextVector = Array.isArray(embedding) && embedding.length > 0 ? new Float32Array(embedding) : null;
821
857
  if (isAutocompleteDebugEnabled()) {
822
858
  // eslint-disable-next-line no-console
@@ -862,7 +898,7 @@ export var createLocalSlowLaneClient = function createLocalSlowLaneClient() {
862
898
  case 5:
863
899
  _context3.prev = 5;
864
900
  _t2 = _context3["catch"](2);
865
- if (!(requestId < latestRequestId)) {
901
+ if (!(requestId < latestRequestId || destroyed)) {
866
902
  _context3.next = 6;
867
903
  break;
868
904
  }
@@ -893,10 +929,48 @@ export var createLocalSlowLaneClient = function createLocalSlowLaneClient() {
893
929
 
894
930
  // ── Context update (debounced) ─────────────────────────────────────────
895
931
 
932
+ var _startInference = function startInference(text, requestId) {
933
+ // Self-contained guard: never start a new inference cycle after teardown,
934
+ // regardless of caller discipline.
935
+ if (destroyed) {
936
+ return;
937
+ }
938
+ inferenceInFlight = true;
939
+ activeInferenceText = text;
940
+ activeInferenceRequestId = requestId;
941
+ void ensureEngineInitialized().then(function () {
942
+ return runInference(text, requestId);
943
+ }).catch(function () {}).finally(function () {
944
+ inferenceInFlight = false;
945
+ activeInferenceText = null;
946
+ activeInferenceRequestId = -1;
947
+ var next = pendingInference;
948
+ pendingInference = null;
949
+ if (next && !destroyed) {
950
+ _startInference(next.text, next.requestId);
951
+ }
952
+ });
953
+ };
896
954
  var doUpdateContext = function doUpdateContext(text) {
955
+ var _pendingInference;
897
956
  if (destroyed || !text || text.trim().length === 0) {
898
957
  return;
899
958
  }
959
+ if (inferenceInFlight && text === activeInferenceText) {
960
+ // The latest desired text already matches the in-flight inference, so
961
+ // re-running it would be wasted work. But an intermediate keystroke may
962
+ // have bumped `latestRequestId` past the in-flight request (and then been
963
+ // coalesced away), which would cause runInference to discard the
964
+ // still-current result as stale. Pin `latestRequestId` back to the active
965
+ // request so its result is accepted, and drop any now-superseded pending
966
+ // request.
967
+ latestRequestId = activeInferenceRequestId;
968
+ pendingInference = null;
969
+ return;
970
+ }
971
+ if (inferenceInFlight && ((_pendingInference = pendingInference) === null || _pendingInference === void 0 ? void 0 : _pendingInference.text) === text) {
972
+ return;
973
+ }
900
974
  var requestId = ++requestCounter;
901
975
  latestRequestId = requestId;
902
976
  if (isAutocompleteDebugEnabled()) {
@@ -910,14 +984,27 @@ export var createLocalSlowLaneClient = function createLocalSlowLaneClient() {
910
984
  // eslint-disable-next-line no-console
911
985
  console.groupEnd();
912
986
  }
913
- void ensureEngineInitialized().then(function () {
914
- return runInference(text, requestId);
915
- }).catch(function () {});
987
+ if (inferenceInFlight) {
988
+ pendingInference = {
989
+ text: text,
990
+ requestId: requestId
991
+ };
992
+ return;
993
+ }
994
+ _startInference(text, requestId);
916
995
  };
917
996
  var updateContextDebounced = function updateContextDebounced(text) {
918
997
  if (debounceTimer) {
919
998
  clearTimeout(debounceTimer);
920
999
  }
1000
+ if (inferenceInFlight) {
1001
+ pendingInference = null;
1002
+ if (text === activeInferenceText) {
1003
+ latestRequestId = activeInferenceRequestId;
1004
+ lastRequestedText = text;
1005
+ return;
1006
+ }
1007
+ }
921
1008
  lastRequestedText = text;
922
1009
  debounceTimer = setTimeout(function () {
923
1010
  debounceTimer = null;
@@ -956,6 +1043,10 @@ export var createLocalSlowLaneClient = function createLocalSlowLaneClient() {
956
1043
  unloadEngine(engineToUnload);
957
1044
  }
958
1045
  engineInitPromise = null;
1046
+ inferenceInFlight = false;
1047
+ activeInferenceText = null;
1048
+ activeInferenceRequestId = -1;
1049
+ pendingInference = null;
959
1050
  storedContextVector = null;
960
1051
  storedLmLogits = null;
961
1052
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-autocomplete",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "description": "Client-side text autocomplete plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -35,7 +35,7 @@
35
35
  "wink-nlp": "^2.4.0"
36
36
  },
37
37
  "peerDependencies": {
38
- "@atlaskit/editor-common": "^115.6.0",
38
+ "@atlaskit/editor-common": "^115.7.0",
39
39
  "@atlaskit/editor-plugin-analytics": "^11.0.0",
40
40
  "react": "^18.2.0"
41
41
  },