@atlaskit/editor-plugin-autocomplete 3.3.0 → 3.4.1

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,21 @@
1
1
  # @atlaskit/editor-plugin-autocomplete
2
2
 
3
+ ## 3.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`4129a00a1ae04`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/4129a00a1ae04) -
8
+ Add `completionSource` attribute to contextual typeahead analytics events to distinguish between
9
+ cold (frequency-only), server slow-lane, and on-device local LLM scoring paths
10
+ - Updated dependencies
11
+
12
+ ## 3.4.0
13
+
14
+ ### Minor Changes
15
+
16
+ - [`b6fb60030b3b9`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/b6fb60030b3b9) -
17
+ Improve local autocomplete scheduling latency
18
+
3
19
  ## 3.3.0
4
20
 
5
21
  ### 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.
@@ -199,6 +223,18 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
199
223
  var dismissedContext = null;
200
224
  var lastSuggestionTypedLength = 0;
201
225
  var lastSuggestionLength = 0;
226
+
227
+ /**
228
+ * cold → no slow-lane vector received yet; frequency-only trie scoring
229
+ * server → server slow-lane API returned the context vector
230
+ * localLlm → on-device WebGPU/MLC model returned the context vector
231
+ */
232
+ var getCompletionSource = function getCompletionSource() {
233
+ if (!slowLaneClient.getContextVector()) {
234
+ return 'cold';
235
+ }
236
+ return options !== null && options !== void 0 && options.useLocalModel ? 'localLlm' : 'server';
237
+ };
202
238
  var fireSuggestionDismissedAnalytics = function fireSuggestionDismissedAnalytics(reason) {
203
239
  var _api$analytics;
204
240
  api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || _api$analytics.actions.fireAnalyticsEvent({
@@ -206,6 +242,7 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
206
242
  actionSubject: _analytics.ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
207
243
  eventType: _analytics.EVENT_TYPE.TRACK,
208
244
  attributes: {
245
+ completionSource: getCompletionSource(),
209
246
  reason: reason
210
247
  }
211
248
  });
@@ -220,6 +257,7 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
220
257
  actionSubject: _analytics.ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
221
258
  eventType: _analytics.EVENT_TYPE.TRACK,
222
259
  attributes: {
260
+ completionSource: getCompletionSource(),
223
261
  suggestionLength: suggestionLength,
224
262
  typedLength: typedLength,
225
263
  kssDelta: kssDelta
@@ -227,12 +265,125 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
227
265
  });
228
266
  };
229
267
  var slowLaneClient = options !== null && options !== void 0 && options.useLocalModel ? (0, _localSlowLaneClient.createLocalSlowLaneClient)({
230
- debounceMs: 300
268
+ debounceMs: LOCAL_SLOW_LANE_DEBOUNCE_MS
231
269
  }) : (0, _slowLaneClient.createSlowLaneClient)({
232
270
  baseUrl: '',
233
- debounceMs: 300
271
+ debounceMs: NETWORK_SLOW_LANE_DEBOUNCE_MS
234
272
  });
235
273
  (0, _slowLaneClient.setDefaultSlowLaneClient)(slowLaneClient);
274
+ var contextRequestInFlight = false;
275
+ var lastContextRefreshAt = 0;
276
+ // Bounds the word-boundary retry loop so it terminates even when the editor is
277
+ // not in a comment thread (parentCommentContent never resolves).
278
+ var wordBoundaryRefreshAttempts = 0;
279
+ // Set when the plugin is torn down so in-flight getContext() resolutions don't
280
+ // mutate the global text-predictor state after destruction.
281
+ var destroyed = false;
282
+ var ingestedContextTexts = new Set();
283
+ var logContextResolved = function logContextResolved(source, context) {
284
+ var _context$parentCommen, _context$siblingComme4, _context$siblingComme5;
285
+ if (!(0, _debugMode.isAutocompleteDebugEnabled)()) {
286
+ return;
287
+ }
288
+
289
+ // eslint-disable-next-line no-console
290
+ console.log('%c[Autocomplete] %cgetContext resolved', 'color: #00b8d9; font-weight: bold;', 'color: inherit;', {
291
+ source: source,
292
+ hasParentComment: !!(context !== null && context !== void 0 && context.parentCommentContent),
293
+ parentCommentPreview: context === null || context === void 0 || (_context$parentCommen = context.parentCommentContent) === null || _context$parentCommen === void 0 ? void 0 : _context$parentCommen.slice(0, 80),
294
+ 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,
295
+ hasFullPage: !!(context !== null && context !== void 0 && context.fullPageContent)
296
+ });
297
+ };
298
+ var applyContext = function applyContext(context) {
299
+ var _context$siblingComme6;
300
+ // Merge rather than replace: the word-boundary retry may resolve only a
301
+ // late-arriving field (e.g. parentCommentContent) without re-sending
302
+ // fullPageContent, so replacing would drop previously resolved context.
303
+ // Strip undefined values first so a field explicitly set to undefined by
304
+ // getContext doesn't overwrite a previously resolved value.
305
+ var definedContext = Object.fromEntries(Object.entries(context).filter(function (_ref) {
306
+ var _ref2 = (0, _slicedToArray2.default)(_ref, 2),
307
+ value = _ref2[1];
308
+ return value !== undefined;
309
+ }));
310
+ resolvedContext = _objectSpread(_objectSpread({}, resolvedContext), definedContext);
311
+ var ingestContextText = function ingestContextText(text) {
312
+ if (!text || ingestedContextTexts.has(text)) {
313
+ return;
314
+ }
315
+ ingestedContextTexts.add(text);
316
+ // Evict oldest entries (Set preserves insertion order) to bound memory.
317
+ while (ingestedContextTexts.size > MAX_INGESTED_CONTEXT_TEXTS) {
318
+ var oldest = ingestedContextTexts.values().next().value;
319
+ if (oldest === undefined) {
320
+ break;
321
+ }
322
+ ingestedContextTexts.delete(oldest);
323
+ }
324
+ (0, _textPredictor.ingestDocumentPage)(text);
325
+ };
326
+ ingestContextText(context.fullPageContent);
327
+ ingestContextText(context.parentCommentContent);
328
+ var _iterator = _createForOfIteratorHelper((_context$siblingComme6 = context.siblingCommentsContents) !== null && _context$siblingComme6 !== void 0 ? _context$siblingComme6 : []),
329
+ _step;
330
+ try {
331
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
332
+ var siblingCommentContent = _step.value;
333
+ ingestContextText(siblingCommentContent);
334
+ }
335
+
336
+ // Context arrived after word boundaries may already have fired. Re-send
337
+ // slow-lane context immediately so the next inference includes the thread.
338
+ } catch (err) {
339
+ _iterator.e(err);
340
+ } finally {
341
+ _iterator.f();
342
+ }
343
+ if (currentView) {
344
+ slowLaneClient.updateContext(buildSlowLaneText(currentView.state.doc.textContent, resolvedContext));
345
+ }
346
+ };
347
+
348
+ /**
349
+ * Returns true when a fetch was actually started, false when it was skipped
350
+ * (no getContext, a request already in flight, or throttled). Callers that
351
+ * track a retry budget should only count attempts where this returns true.
352
+ */
353
+ var refreshContext = function refreshContext(_ref3) {
354
+ var source = _ref3.source,
355
+ _ref3$allowThrottle = _ref3.allowThrottle,
356
+ allowThrottle = _ref3$allowThrottle === void 0 ? true : _ref3$allowThrottle;
357
+ if (!(options !== null && options !== void 0 && options.getContext) || contextRequestInFlight) {
358
+ return false;
359
+ }
360
+ var now = Date.now();
361
+ if (allowThrottle && now - lastContextRefreshAt < CONTEXT_REFRESH_THROTTLE_MS) {
362
+ return false;
363
+ }
364
+ contextRequestInFlight = true;
365
+ lastContextRefreshAt = now;
366
+ options.getContext().then(function (context) {
367
+ // Bail if the plugin was destroyed while the fetch was in flight —
368
+ // applyContext mutates global text-predictor state we must not touch
369
+ // after teardown.
370
+ if (destroyed) {
371
+ return;
372
+ }
373
+ logContextResolved(source, context);
374
+ if (!context) {
375
+ return;
376
+ }
377
+ applyContext(context);
378
+ }).catch(function (error) {
379
+ (0, _monitoring.logException)(error, {
380
+ location: 'editor-plugin-autocomplete/getContext'
381
+ });
382
+ }).finally(function () {
383
+ contextRequestInFlight = false;
384
+ });
385
+ return true;
386
+ };
236
387
 
237
388
  /**
238
389
  * Schedule a prediction after a short debounce.
@@ -273,7 +424,10 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
273
424
  api === null || api === void 0 || (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 || _api$analytics3.actions.fireAnalyticsEvent({
274
425
  action: _analytics.ACTION.SUGGESTION_VIEWED,
275
426
  actionSubject: _analytics.ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
276
- eventType: _analytics.EVENT_TYPE.TRACK
427
+ eventType: _analytics.EVENT_TYPE.TRACK,
428
+ attributes: {
429
+ completionSource: getCompletionSource()
430
+ }
277
431
  });
278
432
  }
279
433
  }
@@ -404,26 +558,10 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
404
558
  });
405
559
  if (!hasIngestedPage) {
406
560
  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
- }
561
+ refreshContext({
562
+ source: 'focus',
563
+ allowThrottle: false
564
+ });
427
565
  }
428
566
  return false;
429
567
  }
@@ -432,6 +570,7 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
432
570
  view: function view() {
433
571
  return {
434
572
  update: function update(view, prevState) {
573
+ currentView = view;
435
574
  if (!prevState.doc.eq(view.state.doc)) {
436
575
  if (justAccepted) {
437
576
  justAccepted = false;
@@ -447,18 +586,36 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
447
586
  maybeUpdateSessionFrequency(view, prevState);
448
587
  var textBefore = getTextBeforeCursor(view.state);
449
588
  if ((0, _slowLaneClient.isWordBoundary)(textBefore)) {
589
+ var _resolvedContext;
450
590
  slowLaneClient.updateContext(buildSlowLaneText(view.state.doc.textContent, resolvedContext));
591
+
592
+ // Context may not have resolved on first focus (e.g. comment
593
+ // thread still loading). Retry on word boundaries until we have
594
+ // the parent comment, throttled so we don't refetch constantly
595
+ // and capped so non-comment editors stop retrying entirely.
596
+ if (!((_resolvedContext = resolvedContext) !== null && _resolvedContext !== void 0 && _resolvedContext.parentCommentContent) && wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS) {
597
+ // Only count the attempt when a fetch actually started, so an
598
+ // in-flight or throttled no-op doesn't burn the retry budget.
599
+ if (refreshContext({
600
+ source: 'word-boundary'
601
+ })) {
602
+ wordBoundaryRefreshAttempts++;
603
+ }
604
+ }
451
605
  }
452
606
  schedulePrediction(view);
453
607
  }
454
608
  },
455
609
  destroy: function destroy() {
610
+ destroyed = true;
611
+ currentView = null;
456
612
  if (debounceTimer) {
457
613
  clearTimeout(debounceTimer);
458
614
  }
459
615
  if (hasDestroy(slowLaneClient)) {
460
616
  slowLaneClient.destroy();
461
617
  }
618
+ ingestedContextTexts.clear();
462
619
  (0, _slowLaneClient.setDefaultSlowLaneClient)(null);
463
620
  }
464
621
  };
@@ -622,6 +622,14 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
622
622
  var lastRequestedText = '';
623
623
  var requestCounter = 0;
624
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;
625
633
  var ready = false;
626
634
  var destroyed = false;
627
635
  var initFailed = false;
@@ -902,7 +910,7 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
902
910
  case 5:
903
911
  _context3.prev = 5;
904
912
  _t2 = _context3["catch"](2);
905
- if (!(requestId < latestRequestId)) {
913
+ if (!(requestId < latestRequestId || destroyed)) {
906
914
  _context3.next = 6;
907
915
  break;
908
916
  }
@@ -933,10 +941,48 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
933
941
 
934
942
  // ── Context update (debounced) ─────────────────────────────────────────
935
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
+ };
936
966
  var doUpdateContext = function doUpdateContext(text) {
967
+ var _pendingInference;
937
968
  if (destroyed || !text || text.trim().length === 0) {
938
969
  return;
939
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
+ }
940
986
  var requestId = ++requestCounter;
941
987
  latestRequestId = requestId;
942
988
  if ((0, _debugMode.isAutocompleteDebugEnabled)()) {
@@ -950,14 +996,27 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
950
996
  // eslint-disable-next-line no-console
951
997
  console.groupEnd();
952
998
  }
953
- void ensureEngineInitialized().then(function () {
954
- return runInference(text, requestId);
955
- }).catch(function () {});
999
+ if (inferenceInFlight) {
1000
+ pendingInference = {
1001
+ text: text,
1002
+ requestId: requestId
1003
+ };
1004
+ return;
1005
+ }
1006
+ _startInference(text, requestId);
956
1007
  };
957
1008
  var updateContextDebounced = function updateContextDebounced(text) {
958
1009
  if (debounceTimer) {
959
1010
  clearTimeout(debounceTimer);
960
1011
  }
1012
+ if (inferenceInFlight) {
1013
+ pendingInference = null;
1014
+ if (text === activeInferenceText) {
1015
+ latestRequestId = activeInferenceRequestId;
1016
+ lastRequestedText = text;
1017
+ return;
1018
+ }
1019
+ }
961
1020
  lastRequestedText = text;
962
1021
  debounceTimer = setTimeout(function () {
963
1022
  debounceTimer = null;
@@ -996,6 +1055,10 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
996
1055
  unloadEngine(engineToUnload);
997
1056
  }
998
1057
  engineInitPromise = null;
1058
+ inferenceInFlight = false;
1059
+ activeInferenceText = null;
1060
+ activeInferenceRequestId = -1;
1061
+ pendingInference = null;
999
1062
  storedContextVector = null;
1000
1063
  storedLmLogits = null;
1001
1064
  }