content,internal: delete autocomplete code
Autocomplete no longer works in dev, and the experiment is not active
in any other environment.
Change-Id: I8865539862a4d1b84bf91641e7662d4926d30488
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/279133
Run-TryBot: Julie Qiu <julie@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Trust: Julie Qiu <julie@golang.org>
diff --git a/content/static/html/base.tmpl b/content/static/html/base.tmpl
index f55afed..1af51b1 100644
--- a/content/static/html/base.tmpl
+++ b/content/static/html/base.tmpl
@@ -218,10 +218,3 @@
</iframe>
</noscript>
{{end}}
-
-{{if (.Experiments.IsActive "autocomplete")}}
-<script>
- loadScript("/third_party/autoComplete.js/autoComplete.min.js");
- loadScript("/static/js/completion.min.js");
-</script>
-{{end}}
diff --git a/content/static/html/helpers/_search_bar.tmpl b/content/static/html/helpers/_search_bar.tmpl
index c624dc5..8a4f8c9 100644
--- a/content/static/html/helpers/_search_bar.tmpl
+++ b/content/static/html/helpers/_search_bar.tmpl
@@ -5,14 +5,11 @@
-->
{{define "search"}}
- <div class="SearchForm-container{{if (.Experiments.IsActive "autocomplete")}} Experiment-autoComplete{{end}}">
- <form class="SearchForm" action="/search" role="search" id="js-AutoComplete-parent" aria-owns="AutoComplete-list">
+ <div class="SearchForm-container">
+ <form class="SearchForm" action="/search" role="search">
<div class="SearchForm-firstRow">
- <input class="SearchForm-input js-autoComplete js-searchFocus"
- id="js-AutoComplete-input"
+ <input class="SearchForm-input js-searchFocus"
role="textbox"
- aria-controls="AutoComplete-list"
- aria-autocomplete="list"
aria-label="Search for a package"
type="text"
name="q"
@@ -38,7 +35,7 @@
<button class="Header-searchFormSubmit" aria-label="Search for a package">
<svg class="Header-searchFormSubmitIcon" focusable="false" viewBox="0 0 24 24" aria-hidden="true" role="presentation"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"></path><path fill="none" d="M0 0h24v24H0z"></path></svg>
</button>
- <input class="Header-searchFormInput js-autoComplete js-searchFocus"
+ <input class="Header-searchFormInput js-searchFocus"
aria-label="Search for a package"
type="text"
name="q"
diff --git a/content/static/js/completion.js b/content/static/js/completion.js
deleted file mode 100644
index be98027..0000000
--- a/content/static/js/completion.js
+++ /dev/null
@@ -1,188 +0,0 @@
-/**
- * @license
- * Copyright 2019-2020 The Go Authors. All rights reserved.
- * Use of this source code is governed by a BSD-style
- * license that can be found in the LICENSE file.
- */
-
-document.addEventListener('DOMContentLoaded', () => {
- // To implement autocomplete we use autoComplete.js, but override the
- // navigation controller to be more accessible.
- //
- // Accessibility requirements are based on:
- // https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html
- // This defines the interaction of three elements: the 'combobox', which is
- // the parent of both the text input, and the completion list.
- //
- // The autoComplete.js library assumes a single element in its query
- // selector, unfortunately, so we can't express our autocompletion behavior
- // in terms of classes rather than IDs. See also
- // https://github.com/TarekRaafat/autoComplete.js/issues/82
- const completeInput = document.querySelector('#js-AutoComplete-input');
- const parentForm = document.querySelector('#js-AutoComplete-parent');
- const hideCompletion = () => {
- parentForm.setAttribute('aria-expanded', false);
- // Without removing aria-activedescendant screenreaders will get confused.
- completeInput.removeAttribute('aria-activedescendant');
- };
- const showCompletion = () => {
- parentForm.setAttribute('aria-expanded', true);
- };
- // Accessibility: hide and show the completion dialog on blur and focus.
- completeInput.addEventListener('blur', hideCompletion);
- completeInput.addEventListener('focus', showCompletion);
-
- /**
- * See https://tarekraafat.github.io/autoComplete.js/#/?id=api-configuration
- * for some documentation of the autoComplete.js API. In particular, it
- * allows overriding the resultsList navigation controller.
- *
- * This controller is based on the default implementation here:
- * https://github.com/TarekRaafat/autoComplete.js/blob/v7.2.0/src/views/autoCompleteView.js#L120
- *
- * The primary changes are:
- * + set aria-expanded, aria-selected, and aria-activedescendant attributes
- * where necessary
- * + use aria attributes for styling, rather than classes
- * + add id attributes to the list elements
- * + hide completion on blur, or escape
- * + simplify the code a bit
- *
- * This function is evaluated each time the result list is assembled (i.e.
- * potentially each keypress).
- *
- * @param input is the text input element.
- * @param resultsList is the ul element being constructed.
- * @param sendFeedback is the data feedback function invoked on user
- * selection. "feedback" is the term used for this in autoComplete.js, so
- * we preserve this for consistency.
- * @param resultsValues is the processed data values from the data source.
- */
- const navigation = (evt, input, resultsList, sendFeedback, resultsValues) => {
- const lis = Array.from(resultsList.childNodes);
- let liSelected = undefined;
- const highlightSelection = next => {
- // Clear the existing selected item.
- if (liSelected) {
- liSelected.removeAttribute('aria-selected');
- }
- liSelected = next;
- liSelected.setAttribute('aria-selected', 'true');
- const idx = lis.findIndex(li => li === liSelected);
- input.setAttribute('aria-activedescendant', 'AutoComplete-item-' + idx);
- };
- const onSelection = (event, elem) => {
- sendFeedback({
- event: event,
- query: input.value,
- matches: resultsValues.matches,
- results: resultsValues.list.map(record => record.value),
- selection: resultsValues.list.find(
- value => value.index === Number(elem.getAttribute('data-id'))
- ),
- });
- hideCompletion();
- };
- input.onkeydown = event => {
- const keys = {
- ENTER: 13,
- ESCAPE: 27,
- ARROW_UP: 38,
- ARROW_DOWN: 40,
- };
- let next = undefined; // the next item to highlight
- if (lis.length > 0) {
- switch (event.keyCode) {
- case keys.ARROW_UP:
- showCompletion();
- // Show the last completion item if none are currently selected, or
- // we're at the start of the list.
- next = lis[lis.length - 1];
- if (liSelected && liSelected.previousSibling) {
- next = liSelected.previousSibling;
- }
- highlightSelection(next);
- // If we don't preventDefault here, up and down arrows cause us to
- // also jump to the start or end of the text input.
- event.preventDefault();
- break;
- case keys.ARROW_DOWN:
- showCompletion();
- // Show the first completion item if none are currently selected,
- // or if we're at the end of the list.
- next = lis[0];
- if (liSelected && liSelected.nextSibling) {
- next = liSelected.nextSibling;
- }
- highlightSelection(next);
- // See note above for why this is necessary.
- event.preventDefault();
- break;
- case keys.ENTER:
- if (liSelected) {
- event.preventDefault();
- onSelection(event, liSelected);
- }
- break;
- case keys.ESCAPE:
- // From the aria guide, escape should both hide completion and
- // clear the input. Arguably it might be better to leave the input
- // untouched, which is what Google search does.
- hideCompletion();
- input.value = '';
- break;
- default:
- // Because we've hidden the completion ourselves on escape, we need
- // to show it on any other character.
- showCompletion();
- }
- }
- };
- lis.forEach((selection, i) => {
- // Completion items need id attributes so that they can be referenced by
- // aria-activedescendant.
- selection.setAttribute('id', 'AutoComplete-item-' + i);
- selection.onmousedown = event => {
- onSelection(event, event.currentTarget);
- event.preventDefault();
- };
- });
- };
-
- new autoComplete({
- data: {
- src: async () => {
- const query = completeInput.value;
- const source = await fetch(`/autocomplete?q=${query}`);
- return await source.json();
- },
- // The string we're completing is stored in the 'PackagePath' field of
- // the returned JSON array elements.
- key: ['PackagePath'],
- cache: false,
- },
- threshold: 1, // minimum number of characters before rendering results
- debounce: 100, // in milliseconds
- resultsList: {
- render: true,
- container: source => {
- source.setAttribute('id', 'AutoComplete-list');
- source.classList.add('AutoComplete-list');
- source.setAttribute('role', 'listbox');
- },
- destination: document.querySelector('#js-AutoComplete-parent'),
- position: 'beforeend',
- element: 'ul',
- navigation: navigation,
- },
- highlight: true,
- selector: '#js-AutoComplete-input',
- onSelection: feedback => {
- if (feedback.selection.value.PackagePath) {
- // Navigate directly to the package.
- // TODO (https://golang.org/issue/42955): update ARIA attributes to reflect this.
- window.location.href = '/' + feedback.selection.value.PackagePath;
- }
- },
- });
-});
diff --git a/content/static/js/completion.min.js b/content/static/js/completion.min.js
deleted file mode 100644
index 294db73..0000000
--- a/content/static/js/completion.min.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
-
- Copyright 2019-2020 The Go Authors. All rights reserved.
- Use of this source code is governed by a BSD-style
- license that can be found in the LICENSE file.
-*/
-var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.createTemplateTagFirstArg=function(a){return a.raw=a};$jscomp.createTemplateTagFirstArgWithRaw=function(a,b){a.raw=b;return a};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;$jscomp.ISOLATE_POLYFILLS=!1;
-$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,e){if(a==Array.prototype||a==Object.prototype)return a;a[b]=e.value;return a};$jscomp.getGlobal=function(a){a=["object"==typeof globalThis&&globalThis,a,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var b=0;b<a.length;++b){var e=a[b];if(e&&e.Math==Math)return e}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
-$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";var $jscomp$lookupPolyfilledValue=function(a,b){var e=$jscomp.propertyToPolyfillSymbol[b];if(null==e)return a[b];e=a[e];return void 0!==e?e:a[b]};
-$jscomp.polyfill=function(a,b,e,f){b&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(a,b,e,f):$jscomp.polyfillUnisolated(a,b,e,f))};$jscomp.polyfillUnisolated=function(a,b,e,f){e=$jscomp.global;a=a.split(".");for(f=0;f<a.length-1;f++){var d=a[f];if(!(d in e))return;e=e[d]}a=a[a.length-1];f=e[a];b=b(f);b!=f&&null!=b&&$jscomp.defineProperty(e,a,{configurable:!0,writable:!0,value:b})};
-$jscomp.polyfillIsolated=function(a,b,e,f){var d=a.split(".");a=1===d.length;f=d[0];f=!a&&f in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var k=0;k<d.length-1;k++){var c=d[k];if(!(c in f))return;f=f[c]}d=d[d.length-1];e=$jscomp.IS_SYMBOL_NATIVE&&"es6"===e?f[d]:null;b=b(e);null!=b&&(a?$jscomp.defineProperty($jscomp.polyfills,d,{configurable:!0,writable:!0,value:b}):b!==e&&($jscomp.propertyToPolyfillSymbol[d]=$jscomp.IS_SYMBOL_NATIVE?$jscomp.global.Symbol(d):$jscomp.POLYFILL_PREFIX+d,d=
-$jscomp.propertyToPolyfillSymbol[d],$jscomp.defineProperty(f,d,{configurable:!0,writable:!0,value:b})))};$jscomp.underscoreProtoCanBeSet=function(){var a={a:!0},b={};try{return b.__proto__=a,b.a}catch(e){}return!1};$jscomp.setPrototypeOf=$jscomp.TRUST_ES6_POLYFILLS&&"function"==typeof Object.setPrototypeOf?Object.setPrototypeOf:$jscomp.underscoreProtoCanBeSet()?function(a,b){a.__proto__=b;if(a.__proto__!==b)throw new TypeError(a+" is not extensible");return a}:null;
-$jscomp.arrayIteratorImpl=function(a){var b=0;return function(){return b<a.length?{done:!1,value:a[b++]}:{done:!0}}};$jscomp.arrayIterator=function(a){return{next:$jscomp.arrayIteratorImpl(a)}};$jscomp.makeIterator=function(a){var b="undefined"!=typeof Symbol&&Symbol.iterator&&a[Symbol.iterator];return b?b.call(a):$jscomp.arrayIterator(a)};$jscomp.generator={};
-$jscomp.generator.ensureIteratorResultIsObject_=function(a){if(!(a instanceof Object))throw new TypeError("Iterator result "+a+" is not an object");};$jscomp.generator.Context=function(){this.isRunning_=!1;this.yieldAllIterator_=null;this.yieldResult=void 0;this.nextAddress=1;this.finallyAddress_=this.catchAddress_=0;this.finallyContexts_=this.abruptCompletion_=null};
-$jscomp.generator.Context.prototype.start_=function(){if(this.isRunning_)throw new TypeError("Generator is already running");this.isRunning_=!0};$jscomp.generator.Context.prototype.stop_=function(){this.isRunning_=!1};$jscomp.generator.Context.prototype.jumpToErrorHandler_=function(){this.nextAddress=this.catchAddress_||this.finallyAddress_};$jscomp.generator.Context.prototype.next_=function(a){this.yieldResult=a};
-$jscomp.generator.Context.prototype.throw_=function(a){this.abruptCompletion_={exception:a,isException:!0};this.jumpToErrorHandler_()};$jscomp.generator.Context.prototype.return=function(a){this.abruptCompletion_={return:a};this.nextAddress=this.finallyAddress_};$jscomp.generator.Context.prototype.jumpThroughFinallyBlocks=function(a){this.abruptCompletion_={jumpTo:a};this.nextAddress=this.finallyAddress_};$jscomp.generator.Context.prototype.yield=function(a,b){this.nextAddress=b;return{value:a}};
-$jscomp.generator.Context.prototype.yieldAll=function(a,b){a=$jscomp.makeIterator(a);var e=a.next();$jscomp.generator.ensureIteratorResultIsObject_(e);if(e.done)this.yieldResult=e.value,this.nextAddress=b;else return this.yieldAllIterator_=a,this.yield(e.value,b)};$jscomp.generator.Context.prototype.jumpTo=function(a){this.nextAddress=a};$jscomp.generator.Context.prototype.jumpToEnd=function(){this.nextAddress=0};
-$jscomp.generator.Context.prototype.setCatchFinallyBlocks=function(a,b){this.catchAddress_=a;void 0!=b&&(this.finallyAddress_=b)};$jscomp.generator.Context.prototype.setFinallyBlock=function(a){this.catchAddress_=0;this.finallyAddress_=a||0};$jscomp.generator.Context.prototype.leaveTryBlock=function(a,b){this.nextAddress=a;this.catchAddress_=b||0};
-$jscomp.generator.Context.prototype.enterCatchBlock=function(a){this.catchAddress_=a||0;a=this.abruptCompletion_.exception;this.abruptCompletion_=null;return a};$jscomp.generator.Context.prototype.enterFinallyBlock=function(a,b,e){e?this.finallyContexts_[e]=this.abruptCompletion_:this.finallyContexts_=[this.abruptCompletion_];this.catchAddress_=a||0;this.finallyAddress_=b||0};
-$jscomp.generator.Context.prototype.leaveFinallyBlock=function(a,b){b=this.finallyContexts_.splice(b||0)[0];if(b=this.abruptCompletion_=this.abruptCompletion_||b){if(b.isException)return this.jumpToErrorHandler_();void 0!=b.jumpTo&&this.finallyAddress_<b.jumpTo?(this.nextAddress=b.jumpTo,this.abruptCompletion_=null):this.nextAddress=this.finallyAddress_}else this.nextAddress=a};$jscomp.generator.Context.prototype.forIn=function(a){return new $jscomp.generator.Context.PropertyIterator(a)};
-$jscomp.generator.Context.PropertyIterator=function(a){this.object_=a;this.properties_=[];for(var b in a)this.properties_.push(b);this.properties_.reverse()};$jscomp.generator.Context.PropertyIterator.prototype.getNext=function(){for(;0<this.properties_.length;){var a=this.properties_.pop();if(a in this.object_)return a}return null};$jscomp.generator.Engine_=function(a){this.context_=new $jscomp.generator.Context;this.program_=a};
-$jscomp.generator.Engine_.prototype.next_=function(a){this.context_.start_();if(this.context_.yieldAllIterator_)return this.yieldAllStep_(this.context_.yieldAllIterator_.next,a,this.context_.next_);this.context_.next_(a);return this.nextStep_()};
-$jscomp.generator.Engine_.prototype.return_=function(a){this.context_.start_();var b=this.context_.yieldAllIterator_;if(b)return this.yieldAllStep_("return"in b?b["return"]:function(e){return{value:e,done:!0}},a,this.context_.return);this.context_.return(a);return this.nextStep_()};
-$jscomp.generator.Engine_.prototype.throw_=function(a){this.context_.start_();if(this.context_.yieldAllIterator_)return this.yieldAllStep_(this.context_.yieldAllIterator_["throw"],a,this.context_.next_);this.context_.throw_(a);return this.nextStep_()};
-$jscomp.generator.Engine_.prototype.yieldAllStep_=function(a,b,e){try{var f=a.call(this.context_.yieldAllIterator_,b);$jscomp.generator.ensureIteratorResultIsObject_(f);if(!f.done)return this.context_.stop_(),f;var d=f.value}catch(k){return this.context_.yieldAllIterator_=null,this.context_.throw_(k),this.nextStep_()}this.context_.yieldAllIterator_=null;e.call(this.context_,d);return this.nextStep_()};
-$jscomp.generator.Engine_.prototype.nextStep_=function(){for(;this.context_.nextAddress;)try{var a=this.program_(this.context_);if(a)return this.context_.stop_(),{value:a.value,done:!1}}catch(b){this.context_.yieldResult=void 0,this.context_.throw_(b)}this.context_.stop_();if(this.context_.abruptCompletion_){a=this.context_.abruptCompletion_;this.context_.abruptCompletion_=null;if(a.isException)throw a.exception;return{value:a.return,done:!0}}return{value:void 0,done:!0}};
-$jscomp.generator.Generator_=function(a){this.next=function(b){return a.next_(b)};this.throw=function(b){return a.throw_(b)};this.return=function(b){return a.return_(b)};this[Symbol.iterator]=function(){return this}};$jscomp.generator.createGenerator=function(a,b){b=new $jscomp.generator.Generator_(new $jscomp.generator.Engine_(b));$jscomp.setPrototypeOf&&a.prototype&&$jscomp.setPrototypeOf(b,a.prototype);return b};
-$jscomp.asyncExecutePromiseGenerator=function(a){function b(f){return a.next(f)}function e(f){return a.throw(f)}return new Promise(function(f,d){function k(c){c.done?f(c.value):Promise.resolve(c.value).then(b,e).then(k,d)}k(a.next())})};$jscomp.asyncExecutePromiseGeneratorFunction=function(a){return $jscomp.asyncExecutePromiseGenerator(a())};$jscomp.asyncExecutePromiseGeneratorProgram=function(a){return $jscomp.asyncExecutePromiseGenerator(new $jscomp.generator.Generator_(new $jscomp.generator.Engine_(a)))};
-$jscomp.initSymbol=function(){};$jscomp.polyfill("Symbol",function(a){if(a)return a;var b=function(d,k){this.$jscomp$symbol$id_=d;$jscomp.defineProperty(this,"description",{configurable:!0,writable:!0,value:k})};b.prototype.toString=function(){return this.$jscomp$symbol$id_};var e=0,f=function(d){if(this instanceof f)throw new TypeError("Symbol is not a constructor");return new b("jscomp_symbol_"+(d||"")+"_"+e++,d)};return f},"es6","es3");$jscomp.initSymbolIterator=function(){};
-$jscomp.polyfill("Symbol.iterator",function(a){if(a)return a;a=Symbol("Symbol.iterator");for(var b="Array Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Uint32Array Float32Array Float64Array".split(" "),e=0;e<b.length;e++){var f=$jscomp.global[b[e]];"function"===typeof f&&"function"!=typeof f.prototype[a]&&$jscomp.defineProperty(f.prototype,a,{configurable:!0,writable:!0,value:function(){return $jscomp.iteratorPrototype($jscomp.arrayIteratorImpl(this))}})}return a},"es6",
-"es3");$jscomp.initSymbolAsyncIterator=function(){};$jscomp.iteratorPrototype=function(a){a={next:a};a[Symbol.iterator]=function(){return this};return a};$jscomp.FORCE_POLYFILL_PROMISE=!1;
-$jscomp.polyfill("Promise",function(a){function b(){this.batch_=null}function e(c){return c instanceof d?c:new d(function(g,h){g(c)})}if(a&&!$jscomp.FORCE_POLYFILL_PROMISE)return a;b.prototype.asyncExecute=function(c){if(null==this.batch_){this.batch_=[];var g=this;this.asyncExecuteFunction(function(){g.executeBatch_()})}this.batch_.push(c)};var f=$jscomp.global.setTimeout;b.prototype.asyncExecuteFunction=function(c){f(c,0)};b.prototype.executeBatch_=function(){for(;this.batch_&&this.batch_.length;){var c=
-this.batch_;this.batch_=[];for(var g=0;g<c.length;++g){var h=c[g];c[g]=null;try{h()}catch(l){this.asyncThrow_(l)}}}this.batch_=null};b.prototype.asyncThrow_=function(c){this.asyncExecuteFunction(function(){throw c;})};var d=function(c){this.state_=0;this.result_=void 0;this.onSettledCallbacks_=[];var g=this.createResolveAndReject_();try{c(g.resolve,g.reject)}catch(h){g.reject(h)}};d.prototype.createResolveAndReject_=function(){function c(l){return function(m){h||(h=!0,l.call(g,m))}}var g=this,h=!1;
-return{resolve:c(this.resolveTo_),reject:c(this.reject_)}};d.prototype.resolveTo_=function(c){if(c===this)this.reject_(new TypeError("A Promise cannot resolve to itself"));else if(c instanceof d)this.settleSameAsPromise_(c);else{a:switch(typeof c){case "object":var g=null!=c;break a;case "function":g=!0;break a;default:g=!1}g?this.resolveToNonPromiseObj_(c):this.fulfill_(c)}};d.prototype.resolveToNonPromiseObj_=function(c){var g=void 0;try{g=c.then}catch(h){this.reject_(h);return}"function"==typeof g?
-this.settleSameAsThenable_(g,c):this.fulfill_(c)};d.prototype.reject_=function(c){this.settle_(2,c)};d.prototype.fulfill_=function(c){this.settle_(1,c)};d.prototype.settle_=function(c,g){if(0!=this.state_)throw Error("Cannot settle("+c+", "+g+"): Promise already settled in state"+this.state_);this.state_=c;this.result_=g;this.executeOnSettledCallbacks_()};d.prototype.executeOnSettledCallbacks_=function(){if(null!=this.onSettledCallbacks_){for(var c=0;c<this.onSettledCallbacks_.length;++c)k.asyncExecute(this.onSettledCallbacks_[c]);
-this.onSettledCallbacks_=null}};var k=new b;d.prototype.settleSameAsPromise_=function(c){var g=this.createResolveAndReject_();c.callWhenSettled_(g.resolve,g.reject)};d.prototype.settleSameAsThenable_=function(c,g){var h=this.createResolveAndReject_();try{c.call(g,h.resolve,h.reject)}catch(l){h.reject(l)}};d.prototype.then=function(c,g){function h(q,n){return"function"==typeof q?function(p){try{l(q(p))}catch(r){m(r)}}:n}var l,m,t=new d(function(q,n){l=q;m=n});this.callWhenSettled_(h(c,l),h(g,m));return t};
-d.prototype.catch=function(c){return this.then(void 0,c)};d.prototype.callWhenSettled_=function(c,g){function h(){switch(l.state_){case 1:c(l.result_);break;case 2:g(l.result_);break;default:throw Error("Unexpected state: "+l.state_);}}var l=this;null==this.onSettledCallbacks_?k.asyncExecute(h):this.onSettledCallbacks_.push(h)};d.resolve=e;d.reject=function(c){return new d(function(g,h){h(c)})};d.race=function(c){return new d(function(g,h){for(var l=$jscomp.makeIterator(c),m=l.next();!m.done;m=l.next())e(m.value).callWhenSettled_(g,
-h)})};d.all=function(c){var g=$jscomp.makeIterator(c),h=g.next();return h.done?e([]):new d(function(l,m){function t(p){return function(r){q[p]=r;n--;0==n&&l(q)}}var q=[],n=0;do q.push(void 0),n++,e(h.value).callWhenSettled_(t(q.length-1),m),h=g.next();while(!h.done)})};return d},"es6","es3");
-$jscomp.polyfill("Array.from",function(a){return a?a:function(b,e,f){e=null!=e?e:function(g){return g};var d=[],k="undefined"!=typeof Symbol&&Symbol.iterator&&b[Symbol.iterator];if("function"==typeof k){b=k.call(b);for(var c=0;!(k=b.next()).done;)d.push(e.call(f,k.value,c++))}else for(k=b.length,c=0;c<k;c++)d.push(e.call(f,b[c],c));return d}},"es6","es3");
-$jscomp.findInternal=function(a,b,e){a instanceof String&&(a=String(a));for(var f=a.length,d=0;d<f;d++){var k=a[d];if(b.call(e,k,d,a))return{i:d,v:k}}return{i:-1,v:void 0}};$jscomp.polyfill("Array.prototype.findIndex",function(a){return a?a:function(b,e){return $jscomp.findInternal(this,b,e).i}},"es6","es3");$jscomp.polyfill("Array.prototype.find",function(a){return a?a:function(b,e){return $jscomp.findInternal(this,b,e).v}},"es6","es3");
-document.addEventListener("DOMContentLoaded",function(){var a=document.querySelector("#js-AutoComplete-input"),b=document.querySelector("#js-AutoComplete-parent"),e=function(){b.setAttribute("aria-expanded",!1);a.removeAttribute("aria-activedescendant")},f=function(){b.setAttribute("aria-expanded",!0)};a.addEventListener("blur",e);a.addEventListener("focus",f);new autoComplete({data:{src:function(){var d,k;return $jscomp.asyncExecutePromiseGeneratorProgram(function(c){return 1==c.nextAddress?(d=a.value,
-c.yield(fetch("/autocomplete?q="+d),2)):3!=c.nextAddress?(k=c.yieldResult,c.yield(k.json(),3)):c.return(c.yieldResult)})},key:["PackagePath"],cache:!1},threshold:1,debounce:100,resultsList:{render:!0,container:function(d){d.setAttribute("id","AutoComplete-list");d.classList.add("AutoComplete-list");d.setAttribute("role","listbox")},destination:document.querySelector("#js-AutoComplete-parent"),position:"beforeend",element:"ul",navigation:function(d,k,c,g,h){var l=Array.from(c.childNodes),m=void 0,
-t=function(n){m&&m.removeAttribute("aria-selected");m=n;m.setAttribute("aria-selected","true");n=l.findIndex(function(p){return p===m});k.setAttribute("aria-activedescendant","AutoComplete-item-"+n)},q=function(n,p){g({event:n,query:k.value,matches:h.matches,results:h.list.map(function(r){return r.value}),selection:h.list.find(function(r){return r.index===Number(p.getAttribute("data-id"))})});e()};k.onkeydown=function(n){if(0<l.length)switch(n.keyCode){case 38:f();var p=l[l.length-1];m&&m.previousSibling&&
-(p=m.previousSibling);t(p);n.preventDefault();break;case 40:f();p=l[0];m&&m.nextSibling&&(p=m.nextSibling);t(p);n.preventDefault();break;case 13:m&&(n.preventDefault(),q(n,m));break;case 27:e();k.value="";break;default:f()}};l.forEach(function(n,p){n.setAttribute("id","AutoComplete-item-"+p);n.onmousedown=function(r){q(r,r.currentTarget);r.preventDefault()}})}},highlight:!0,selector:"#js-AutoComplete-input",onSelection:function(d){d.selection.value.PackagePath&&(window.location.href="/"+d.selection.value.PackagePath)}})});
diff --git a/go.mod b/go.mod
index 8c954f1..a55f0ff 100644
--- a/go.mod
+++ b/go.mod
@@ -43,7 +43,6 @@
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
golang.org/x/sys v0.0.0-20200922070232-aee5d888a860 // indirect
golang.org/x/text v0.3.4 // indirect
- golang.org/x/time v0.0.0-20191024005414-555d28b269f0
google.golang.org/api v0.32.0
google.golang.org/genproto v0.0.0-20200923140941-5646d36feee1
google.golang.org/grpc v1.32.0
diff --git a/internal/complete/completion.go b/internal/complete/completion.go
deleted file mode 100644
index 01a3987..0000000
--- a/internal/complete/completion.go
+++ /dev/null
@@ -1,127 +0,0 @@
-// Copyright 2019 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// Package complete defines a Completion type that is used in auto-completion,
-// along with Encode and Decode methods that can be used for storing this type
-// in redis.
-package complete
-
-import (
- "fmt"
- "strconv"
- "strings"
-
- "golang.org/x/pkgsite/internal/derrors"
-)
-
-const keySep = "|"
-
-// Redis keys for completion sorted sets ("indexes"). They are in this package
-// so that they can be accessed by both worker and frontend.
-const (
- KeyPrefix = "completions"
- PopularKey = KeyPrefix + "Popular"
- RemainingKey = KeyPrefix + "Rest"
-)
-
-// Completion holds package data from an auto-completion match.
-type Completion struct {
- // Suffix is the path suffix that matched the compltion input, e.g. a query
- // for "error" would match the suffix "errors" of "github.com/pkg/errors".
- Suffix string
- // ModulePath is the module path of the completion match. We may support
- // matches of the same path in different modules.
- ModulePath string
- // Version is the module version of the completion entry.
- Version string
- // PackagePath is the full import path.
- PackagePath string
- // Importers is the number of importers of this package. It is used for
- // sorting completion results.
- Importers int
-}
-
-// Encode string-encodes a completion for storing in the completion index.
-func (c Completion) Encode() string {
- return strings.Join(c.keyData(), keySep)
-}
-
-func (c Completion) keyData() []string {
- var suffix string
- if strings.HasPrefix(c.PackagePath, c.ModulePath) {
- suffix = strings.TrimPrefix(c.PackagePath, c.ModulePath)
- suffix = "/" + strings.Trim(suffix, "/")
- } else {
- // In the case of the standard library, ModulePath will not be a prefix of
- // PackagePath.
- suffix = c.PackagePath
- }
- return []string{
- c.Suffix,
- strings.TrimRight(c.ModulePath, "/"),
- c.Version,
- suffix,
- // It's important that importers is last in this key, since it is the only
- // datum that changes. By having it last, we reserve the ability to
- // selectively update this entry by deleting the prefix corresponding to
- // the values above.
- strconv.Itoa(c.Importers),
- }
-}
-
-// Decode parses a completion entry from the completions index.
-func Decode(entry string) (_ *Completion, err error) {
- defer derrors.Wrap(&err, "complete.Decode(%q)", entry)
- parts := strings.Split(entry, "|")
- if len(parts) != 5 {
- return nil, fmt.Errorf("got %d parts, want 5", len(parts))
- }
- c := &Completion{
- Suffix: parts[0],
- ModulePath: parts[1],
- Version: parts[2],
- }
- suffix := parts[3]
- if strings.HasPrefix(suffix, "/") {
- c.PackagePath = strings.Trim(c.ModulePath+suffix, "/")
- } else {
- c.PackagePath = suffix
- }
- importers, err := strconv.Atoi(parts[4])
- if err != nil {
- return nil, fmt.Errorf("error parsing importers: %v", err)
- }
- c.Importers = importers
- return c, nil
-}
-
-// PathCompletions generates completion entries for all possible suffixes of
-// partial.PackagePath.
-func PathCompletions(partial Completion) []*Completion {
- suffs := pathSuffixes(partial.PackagePath)
- var cs []*Completion
- for _, pref := range suffs {
- var next = partial
- next.Suffix = pref
- cs = append(cs, &next)
- }
- return cs
-}
-
-// pathSuffixes returns a slice of all path suffixes of a '/'-separated path,
-// including the full path itself. i.e.
-// pathSuffixes("foo/bar") = []string{"foo/bar", "bar"}
-func pathSuffixes(path string) []string {
- path = strings.ToLower(path)
- var prefs []string
- for len(path) > 0 {
- prefs = append(prefs, path)
- i := strings.Index(path, "/")
- if i < 0 {
- break
- }
- path = path[i+1:]
- }
- return prefs
-}
diff --git a/internal/complete/completion_test.go b/internal/complete/completion_test.go
deleted file mode 100644
index 377386c..0000000
--- a/internal/complete/completion_test.go
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright 2019 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package complete
-
-import (
- "sort"
- "testing"
-
- "github.com/google/go-cmp/cmp"
-)
-
-func TestEncodeDecode(t *testing.T) {
- completions := []Completion{
- {},
- {Version: "foo"},
- {Importers: 42},
- {Suffix: "foo"},
- {ModulePath: "foo"},
- {PackagePath: "github.com/foo/bar/baz"},
- {
- Suffix: "github.com/foo",
- ModulePath: "github.com/foo/bar",
- Version: "v1.2.3",
- PackagePath: "github.com/foo/bar/baz",
- Importers: 101,
- },
- {
- Suffix: "fmt",
- ModulePath: "std",
- Version: "go1.13",
- PackagePath: "fmt",
- Importers: 1234,
- },
- }
- for _, c := range completions {
- encoded := c.Encode()
- decoded, err := Decode(encoded)
- if err != nil {
- t.Fatal(err)
- }
- if diff := cmp.Diff(c, *decoded); diff != "" {
- t.Errorf("[%#v] decoded mismatch (-initial +decoded):\n%s\nencoded: %q", c, diff, encoded)
- }
- }
-}
-
-func TestPathCompletions(t *testing.T) {
- partial := Completion{
- ModulePath: "my.module/foo",
- PackagePath: "my.module/foo/bar",
- Version: "v1.2.3",
- Importers: 123,
- }
- completions := PathCompletions(partial)
- sort.Slice(completions, func(i, j int) bool {
- return len(completions[i].Suffix) < len(completions[j].Suffix)
- })
- wantSuffixes := []string{"bar", "foo/bar", "my.module/foo/bar"}
- if got, want := len(completions), len(wantSuffixes); got != want {
- t.Fatalf("len(pathCompletions(%v)) = %d, want %d", partial, got, want)
- }
- for i, got := range completions {
- want := partial
- want.Suffix = wantSuffixes[i]
- if diff := cmp.Diff(want, *got); diff != "" {
- t.Errorf("completions[%d] mismatch (-want +got)\n%s", i, diff)
- }
- }
-}
-
-func TestPathSuffixes(t *testing.T) {
- tests := []struct {
- path string
- want []string
- }{
- {"foo/Bar/baz", []string{"foo/bar/baz", "bar/baz", "baz"}},
- {"foo", []string{"foo"}},
- {"BAR", []string{"bar"}},
- }
- for _, test := range tests {
- if got := pathSuffixes(test.path); !cmp.Equal(got, test.want) {
- t.Errorf("prefixes(%q) = %v, want %v", test.path, got, test.want)
- }
- }
-}
diff --git a/internal/experiment.go b/internal/experiment.go
index ee2d62d..73abee8 100644
--- a/internal/experiment.go
+++ b/internal/experiment.go
@@ -6,7 +6,6 @@
package internal
const (
- ExperimentAutocomplete = "autocomplete"
ExperimentGetUnitMetaQuery = "get-unit-meta-query"
ExperimentGoldmark = "goldmark"
ExperimentReadmeOutline = "readme-outline"
@@ -16,7 +15,6 @@
// Experiments represents all of the active experiments in the codebase and
// a description of each experiment.
var Experiments = map[string]string{
- ExperimentAutocomplete: "Enable autocomplete with search.",
ExperimentGetUnitMetaQuery: "Enable the new get unit meta query, which reads from the paths table.",
ExperimentGoldmark: "Enable the usage of rendering markdown using goldmark instead of blackfriday.",
ExperimentReadmeOutline: "Enable the readme outline in the side nav.",
diff --git a/internal/frontend/completion.go b/internal/frontend/completion.go
deleted file mode 100644
index 9123bf9..0000000
--- a/internal/frontend/completion.go
+++ /dev/null
@@ -1,181 +0,0 @@
-// Copyright 2019 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package frontend
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "io"
- "net/http"
- "sort"
- "strings"
-
- "github.com/go-redis/redis/v8"
- "golang.org/x/pkgsite/internal/complete"
- "golang.org/x/pkgsite/internal/derrors"
- "golang.org/x/pkgsite/internal/log"
-)
-
-// handleAutoCompletion handles requests for /autocomplete?q=<input prefix>, by
-// querying redis sorted sets indexing package paths.
-func (s *Server) handleAutoCompletion(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- var completions []*complete.Completion
- if s.cmplClient != nil {
- var err error
- q := r.FormValue("q")
- completions, err = doCompletion(r.Context(), s.cmplClient, strings.ToLower(q), 5)
- if err != nil {
- code := http.StatusInternalServerError
- http.Error(w, http.StatusText(code), code)
- return
- }
- }
- if completions == nil {
- // autocomplete.js complains if the JSON returned by this endpoint is null,
- // so we initialize a non-nil empty array to serialize to an empty JSON
- // array.
- completions = []*complete.Completion{}
- }
- response, err := json.Marshal(completions)
- if err != nil {
- log.Errorf(ctx, "error marshalling completion: json.Marshal: %v", err)
- }
- w.Header().Set("Content-Type", "application/json")
- if _, err := io.Copy(w, bytes.NewReader(response)); err != nil {
- log.Errorf(ctx, "Error copying json buffer to ResponseWriter: %v", err)
- }
-}
-
-// scoredCompletion wraps Completions with a relevancy score, so that they can
-// be sorted.
-type scoredCompletion struct {
- c *complete.Completion
- score int
-}
-
-// doCompletion executes the completion query against redis. This is inspired
-// by http://oldblog.antirez.com/post/autocomplete-with-redis.html, but
-// improved as follows:
-// + Use ZRANGEBYLEX to avoid storing each possible prefix, since that was
-// added to Redis since the original blog post.
-// + Use an additional sorted set that holds popular packages, to improve
-// completion relevancy.
-//
-// We autocomplete the query 'q' as follows
-// 1. Query for popular completions starting with q using ZRANGEBYLEX (more
-// details on this below). We fetch an arbitrary number of results (1000)
-// to bound the amount of work done by redis.
-// 2. Sort the returned completions by our score (a mix of popularity and
-// proximity to the end of the import path), and filter to the top
-// maxResults.
-// 3. If we have maxResults results, we're done. Otherwise do (1) on the index
-// of remaining (unpopular) package paths, add to our result set, and sort
-// again (because unpopular packages might actually score higher than
-// popular packages).
-func doCompletion(ctx context.Context, r *redis.Client, q string, maxResults int) (_ []*complete.Completion, err error) {
- defer derrors.Wrap(&err, "doCompletion(%q, %d)", q, maxResults)
- scored, err := completeWithIndex(ctx, r, q, complete.PopularKey, maxResults)
- if err != nil {
- return nil, err
- }
- if len(scored) < maxResults {
- unpopular, err := completeWithIndex(ctx, r, q, complete.RemainingKey, maxResults-len(scored))
- if err != nil {
- return nil, err
- }
- scored = append(scored, unpopular...)
- // Re-sort, as it is possible that an unpopular completion actually has a
- // higher score than a popular completion due to the weighting for suffix
- // length.
- sort.Slice(scored, func(i, j int) bool {
- return scored[i].score > scored[j].score
- })
- }
- var completions []*complete.Completion
- for _, s := range scored {
- completions = append(completions, s.c)
- }
- return completions, nil
-}
-
-func completeWithIndex(ctx context.Context, r *redis.Client, q, indexKey string, maxResults int) (_ []*scoredCompletion, err error) {
- defer derrors.Wrap(&err, "completeWithIndex(%q, %q, %d)", q, indexKey, maxResults)
-
- // Query for possible completions using ZRANGEBYLEX. See documentation at
- // https://redis.io/commands/zrangebylex
- // Notably, the "(" character in the Min and Max fields means 'exclude this
- // endpoint'.
- // We bound our search in two ways: (1) by setting Max to the smallest string
- // that lexically greater than q but does not start with q, and (2) by
- // setting an arbitrary limit of 1000 results.
- entries, err := r.ZRangeByLex(ctx, indexKey, &redis.ZRangeBy{
- Min: "(" + q,
- Max: "(" + nextPrefix(q),
- Count: 1000,
- }).Result()
- var scored []*scoredCompletion
- for _, entry := range entries {
- c, err := complete.Decode(entry)
- if err != nil {
- return nil, err
- }
- offset := len(strings.Split(entry, "/"))
- s := &scoredCompletion{
- c: c,
- // Weight importers by distance of the matching text from the end of the
- // import path. This is done in an attempt to make results more relevant
- // the closer the match is to the end of the import path. For example, if
- // the user types 'net', we should have some preference for 'net' over
- // 'net/http'. In this case, it actually works out like so:
- // - net has ~68000 importers
- // - net/http has ~130000 importers
- //
- // So the score of 'net' is ~68000 (offset=1), and the score of
- // 'net/http' is ~65000 (130K/2, as offset=2), therefore net should be
- // sorted above 'net/http' in the results.
- //
- // This heuristic is a total guess, but since this is just autocomplete
- // it probably doesn't matter much. In testing, it felt like autocomplete
- // was completing the packages I wanted.
- //
- // The `- offset` term is added to break ties in the case where all
- // completion results have 0 importers.
- score: c.Importers/offset - offset,
- }
- scored = append(scored, s)
- }
- // sort by score descending
- sort.Slice(scored, func(i, j int) bool {
- return scored[i].score > scored[j].score
- })
- if len(scored) > maxResults {
- scored = scored[:maxResults]
- }
- return scored, nil
-}
-
-// nextPrefix returns the first string (according to lexical sorting) that is
-// greater than prefix but does not start with prefix.
-func nextPrefix(prefix string) string {
- // redis strings are ASCII. Note that among printing ASCII characters '!' has
- // the smallest byte value and '~' has the largest byte value. It also so
- // happens that these are both valid characters in a URL.
- if prefix == "" {
- return ""
- }
- lastChar := prefix[len(prefix)-1]
- if lastChar >= '~' {
- // If the last character is '~', there is no greater ascii character so we
- // must move to the previous character to find a lexically greater string
- // that doesn't start with prefix. Note that in the degenerate case where
- // prefix is nothing but twiddles (e.g. "~~~"), we will recurse until we return "",
- // which is acceptable: there is no prefix that satisfies our requirements:
- // all strings greater than "~~~" must also start with "~~~"
- return nextPrefix(prefix[:len(prefix)-1])
- }
- return prefix[:len(prefix)-1] + string(lastChar+1)
-}
diff --git a/internal/frontend/completion_test.go b/internal/frontend/completion_test.go
deleted file mode 100644
index 87403e8..0000000
--- a/internal/frontend/completion_test.go
+++ /dev/null
@@ -1,96 +0,0 @@
-// Copyright 2019 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package frontend
-
-import (
- "context"
- "testing"
-
- "github.com/alicebob/miniredis/v2"
- "github.com/go-redis/redis/v8"
- "github.com/google/go-cmp/cmp"
- "golang.org/x/pkgsite/internal/complete"
-)
-
-func TestAutoCompletion(t *testing.T) {
- ctx := context.Background()
- s, err := miniredis.Run()
- if err != nil {
- t.Fatal(err)
- }
- defer s.Close()
- r := redis.NewClient(&redis.Options{Addr: s.Addr()})
- // For convenence, populate completion data based on detail path -> imports.
- pathData := map[string]int{
- "foo.com/bar@v1.2.3/baz": 123,
- "foo.com/quux@v1.0.0/bark": 10,
- "github.com/something@v2.0.0/foo": 80,
- }
- for k, v := range pathData {
- got, err := parseDetailsURLPath(k)
- if err != nil {
- t.Fatal(err)
- }
- partial := complete.Completion{
- PackagePath: got.fullPath,
- ModulePath: got.modulePath,
- Version: got.requestedVersion,
- Importers: v,
- }
- completions := complete.PathCompletions(partial)
- var zs []*redis.Z
- for _, cmpl := range completions {
- zs = append(zs, &redis.Z{Member: cmpl.Encode()})
- }
- if v > 0 {
- r.ZAdd(ctx, complete.PopularKey, zs...)
- } else {
- r.ZAdd(ctx, complete.RemainingKey, zs...)
- }
- }
- ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
- defer cancel()
-
- tests := []struct {
- q string
- want []string
- }{
- {"baz", []string{"foo.com/bar/baz"}},
- {"bar", []string{"foo.com/bar/baz", "foo.com/quux/bark"}},
- {"foo", []string{"github.com/something/foo", "foo.com/bar/baz"}},
- }
- for _, test := range tests {
- t.Run(test.q, func(t *testing.T) {
- results, err := doCompletion(ctx, r, test.q, 2)
- if err != nil {
- t.Fatal(err)
- }
- var got []string
- for _, res := range results {
- got = append(got, res.PackagePath)
- }
- if diff := cmp.Diff(test.want, got); diff != "" {
- t.Errorf("doCompletion(%q) mismatch (-want +got)\n%s", test.q, diff)
- }
- })
- }
-}
-
-func TestNextPrefix(t *testing.T) {
- tests := []struct {
- prefix, want string
- }{
- {"", ""},
- {"~~~", ""},
- {"aa", "ab"},
- {"aB", "aC"},
- {"a~", "b"},
- }
- for _, test := range tests {
- if got := nextPrefix(test.prefix); got != test.want {
- t.Errorf("nextPrefix(%q) = %q, want %q", test.prefix, got, test.want)
- }
- }
-}
diff --git a/internal/frontend/server.go b/internal/frontend/server.go
index c2151b7..f841446 100644
--- a/internal/frontend/server.go
+++ b/internal/frontend/server.go
@@ -147,7 +147,6 @@
handle("/detail-stats/",
middleware.Stats()(http.StripPrefix("/detail-stats", s.errorHandler(s.serveDetails))))
}
- handle("/autocomplete", http.HandlerFunc(s.handleAutoCompletion))
handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
http.ServeContent(w, r, "", time.Time{}, strings.NewReader(`User-agent: *
diff --git a/internal/middleware/secureheaders.go b/internal/middleware/secureheaders.go
index 24b6704..2fe0d23 100644
--- a/internal/middleware/secureheaders.go
+++ b/internal/middleware/secureheaders.go
@@ -13,7 +13,6 @@
var scriptHashes = []string{
// From content/static/html/base.tmpl
"'sha256-CgM7SjnSbDyuIteS+D1CQuSnzyKwL0qtXLU6ZW2hB+g='",
- "'sha256-qPGTOKPn+niRiNKQIEX0Ktwuj+D+iPQWIxnlhPicw58='",
"'sha256-LIQd8c4GSueKwR3q2fz3AB92cOdy2Ld7ox8pfvMPHns='",
"'sha256-dwce5DnVX7uk6fdvvNxQyLTH/cJrTMDK6zzrdKwdwcg='",
// From content/static/html/pages/badge.tmpl
diff --git a/internal/worker/completion.go b/internal/worker/completion.go
deleted file mode 100644
index 2cde613..0000000
--- a/internal/worker/completion.go
+++ /dev/null
@@ -1,185 +0,0 @@
-// Copyright 2019 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package worker
-
-import (
- "context"
- "database/sql"
- "errors"
- "fmt"
- "net/http"
- "time"
-
- "github.com/go-redis/redis/v8"
- "golang.org/x/pkgsite/internal/complete"
- "golang.org/x/pkgsite/internal/database"
- "golang.org/x/pkgsite/internal/derrors"
- "golang.org/x/pkgsite/internal/log"
-)
-
-const popularCutoff = 50
-
-// handleUpdateRedisIndexes scans recently modified search documents, and
-// updates redis auto completion indexes with data from these documents.
-func (s *Server) handleUpdateRedisIndexes(w http.ResponseWriter, r *http.Request) error {
- ctx := r.Context()
- err := updateRedisIndexes(ctx, s.db.Underlying(), s.redisHAClient, popularCutoff)
- if err != nil {
- return err
- }
- fmt.Fprint(w, "OK")
- return nil
-}
-
-// updateRedisIndexes updates redisClient with autocompletion data from db.
-// cutoff specifies the number of importers at which a package is considered
-// popular, and is passed-in as an argument to facilitate testing.
-func updateRedisIndexes(ctx context.Context, db *database.DB, redisClient *redis.Client, cutoff int) (err error) {
- defer derrors.Wrap(&err, "updateRedisIndexes")
- if redisClient == nil {
- return errors.New("redis HA client is nil")
- }
-
- // For autocompletion, we track two separate "indexes" (sorted sets of
- // package path suffixes): one for popular packages, and one for the
- // remainder, as defined by the popularCutoff const. This allows us to
- // suggest popular completions, even when the user input is short (i.e. we
- // want to suggest 'fmt' when the user types 'f', but don't want to scan all
- // completions that start with the letter 'f').
- //
- // This function scans search documents in the database and builds up a
- // pipeline that writes these two sorted sets to Redis, using timestamped
- // temporary keys, and then renames them to the keys used by the frontend for
- // autocompletion.
- //
- // See https://redis.io/commands/rename for more information on renaming:
- // it's unclear whether renaming is atomic, but we don't really care.
- // Populating these indexes currently takes 1-2 minutes, and renaming takes
- // 1-2 seconds. Even if completions are broken during this 1-2 seconds, it's
- // preferable to them being broken for 1-2 minutes. We could do something
- // more clever, such as updating the completion data in place using
- // ZREMRANGEBYLEX followed by ZADD, but that would be significantly more
- // complicated.
- //
- // One additional concern of this operation is that we temporary double the
- // size of our redis database while we're staging the new completion data.
- // That's fine, but it's dangerous if we ever have a bug and this operation
- // was either not cleaned up properly, or run concurrently. In light of this,
- // we first look for evidence of another update operation currently running,
- // by scanning Redis for keys that match the temporary key pattern.
-
- // Check for an ongoing update operation, as described above.
- tempKeyPattern := fmt.Sprintf("%s*-*", complete.KeyPrefix)
- existing, _, err := redisClient.Scan(ctx, 0, tempKeyPattern, 1).Result()
- if err != nil {
- return fmt.Errorf(`redis error: Scan(%q): %v`, tempKeyPattern, err)
- }
- if len(existing) > 0 {
- return fmt.Errorf("found existing in-progress completion index: %v", existing[0])
- }
-
- // Use temporary timestamped keys while we write the completion data, as it
- // can take ~minutes.
- keyPop := fmt.Sprintf("%s-%s", complete.PopularKey, time.Now().Format(time.RFC3339))
- keyRem := fmt.Sprintf("%s-%s", complete.RemainingKey, time.Now().Format(time.RFC3339))
-
- // Always clean up: DEL succeeds even if the keys have been renamed.
- defer func() {
- if _, err := redisClient.Del(ctx, keyPop).Result(); err != nil {
- log.Errorf(ctx, "redisClient.Del(%q): %v", keyPop, err)
- }
- if _, err := redisClient.Del(ctx, keyRem).Result(); err != nil {
- log.Errorf(ctx, "redisClient.Del(%q): %v", keyRem, err)
- }
- }()
-
- // pipeSize tracks the number of ZADD statements in the pipe.
- pipeSize := 0
- pipe := redisClient.Pipeline()
- defer pipe.Close()
- // flush executes the current pipeline and resets its state.
- flush := func() error {
- log.Infof(ctx, "Writing completion data pipeline of size %d.", pipeSize)
- if _, err := pipe.Exec(ctx); err != nil {
- return fmt.Errorf("redis error: pipe.Exec: %v", err)
- }
- // As of writing this is unnecessary as ExecContext resets the pipeline
- // commands, but since this is not documented functionality we explicitly
- // Discard.
- pipe.Discard()
- pipeSize = 0
- return nil
- }
- // Track whether or not we have any entries in the popular or remaining
- // indexes. This is an edge case, but if we don't insert any entries for a
- // given index the key won't exist and we'll get an error when renaming.
- var (
- haveRemaining bool
- havePopular bool
- )
- // As of writing there were around 5M entries in our index, so writing in
- // batches of 1M should result in ~6 batches.
- const batchSize = 1e6
- // processRow builds up a Redis pipeline as we scan the search_documents
- // table.
- processRow := func(rows *sql.Rows) error {
- var partial complete.Completion
- if err := rows.Scan(&partial.PackagePath, &partial.ModulePath, &partial.Version, &partial.Importers); err != nil {
- return fmt.Errorf("rows.Scan: %v", err)
- }
- cmpls := complete.PathCompletions(partial)
- var zs []*redis.Z
- for _, cmpl := range cmpls {
- zs = append(zs, &redis.Z{Member: cmpl.Encode()})
- }
- switch {
- case partial.Importers >= cutoff:
- havePopular = true
- pipe.ZAdd(ctx, keyPop, zs...)
- default:
- haveRemaining = true
- pipe.ZAdd(ctx, keyRem, zs...)
- }
- pipeSize += len(zs)
- if pipeSize > batchSize {
- if err := flush(); err != nil {
- return err
- }
- }
- return nil
- }
-
- // Here we use the *database.DB rather than a function on postgres.DB,
- // so that we can write to our redis pipeline while we stream results from
- // the DB. Otherwise, we would have to:
- // - add a method on postgres.DB for the trivial query above
- // - add a type (or reuse SearchResult) to hold the subset of search
- // document data used here.
- // - hold two copies of all search results in memory while building the
- // redis pipeline below.
- query := `
- SELECT package_path, module_path, version, imported_by_count
- FROM search_documents`
- if err := db.RunQuery(ctx, query, processRow); err != nil {
- return err
- }
- if err := flush(); err != nil {
- return err
- }
- pipe.Close()
- if havePopular {
- log.Infof(ctx, "Renaming %q to %q", keyPop, complete.PopularKey)
- if _, err := redisClient.Rename(ctx, keyPop, complete.PopularKey).Result(); err != nil {
- return fmt.Errorf(`redis error: Rename(%q, %q): %v`, keyPop, complete.PopularKey, err)
- }
- }
- if haveRemaining {
- log.Infof(ctx, "Renaming %q to %q", keyRem, complete.RemainingKey)
- if _, err := redisClient.Rename(ctx, keyRem, complete.RemainingKey).Result(); err != nil {
- return fmt.Errorf(`redis error: Rename(%q, %q): %v`, keyRem, complete.RemainingKey, err)
- }
- }
- return nil
-}
diff --git a/internal/worker/completion_test.go b/internal/worker/completion_test.go
deleted file mode 100644
index e6388e8..0000000
--- a/internal/worker/completion_test.go
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright 2019 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package worker
-
-import (
- "context"
- "testing"
-
- "github.com/alicebob/miniredis/v2"
- "github.com/go-redis/redis/v8"
- "golang.org/x/pkgsite/internal/complete"
- "golang.org/x/pkgsite/internal/postgres"
- "golang.org/x/pkgsite/internal/testing/sample"
-)
-
-func TestUpdateRedisIndexes(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
- defer cancel()
- defer postgres.ResetTestDB(testDB, t)
- mr, err := miniredis.Run()
- if err != nil {
- t.Fatal(err)
- }
- defer mr.Close()
-
- // Set up a simple test case with two module versions and two packages. The
- // package in v2 imports the package in v1. By setting our 'popular cutoff'
- // to 1, we can force the package in v1 to be considered popular.
- rc := redis.NewClient(&redis.Options{Addr: mr.Addr()})
- m1 := sample.Module("github.com/something", sample.VersionString, "apples/bananas")
- m2 := sample.Module("github.com/something/else", sample.VersionString, "oranges/bananas")
- m2.Units[1].Imports = []string{m1.Units[1].Path}
- if err := testDB.InsertModule(ctx, m1); err != nil {
- t.Fatal(err)
- }
- if err := testDB.InsertModule(ctx, m2); err != nil {
- t.Fatal(err)
- }
- if _, err := testDB.UpdateSearchDocumentsImportedByCount(ctx); err != nil {
- t.Fatal(err)
- }
- if err := updateRedisIndexes(ctx, testDB.Underlying(), rc, 1); err != nil {
- t.Fatal(err)
- }
- popCount, err := rc.ZCount(ctx, complete.PopularKey, "0", "0").Result()
- if err != nil {
- t.Fatal(err)
- }
- // There are 4 path components in github.com/something/apples/bananas
- if popCount != 4 {
- t.Errorf("got %d popular autocompletions, want %d", popCount, 4)
- }
- remCount, err := rc.ZCount(ctx, complete.RemainingKey, "0", "0").Result()
- if err != nil {
- t.Fatal(err)
- }
- // There are 5 path components in github.com/something/else/oranges/bananas
- if remCount != 5 {
- t.Errorf("got %d remaining autocompletions, want %d", remCount, 5)
- }
-}
diff --git a/internal/worker/server.go b/internal/worker/server.go
index 52d7ded..08ac50a 100644
--- a/internal/worker/server.go
+++ b/internal/worker/server.go
@@ -138,10 +138,6 @@
// This endpoint is intended to be invoked periodically by a scheduler.
handle("/update-imported-by-count", rmw(s.errorHandler(s.handleUpdateImportedByCount)))
- // scheduled: download search document data and update the redis sorted
- // set(s) used in auto-completion.
- handle("/update-redis-indexes", rmw(s.errorHandler(s.handleUpdateRedisIndexes)))
-
// task-queue: fetch fetches a module version from the Module Mirror, and
// processes the contents, and inserts it into the database. If a fetch
// request fails for any reason other than an http.StatusInternalServerError,