| Index: lib/subscriptionClasses.js |
| =================================================================== |
| --- a/lib/subscriptionClasses.js |
| +++ b/lib/subscriptionClasses.js |
| @@ -31,19 +31,25 @@ |
| * |
| * @param {string} url download location of the subscription |
| * @param {string} [title] title of the filter subscription |
| * @constructor |
| */ |
| function Subscription(url, title) |
| { |
| this.url = url; |
| - this.filters = []; |
| + |
| + this._filterText = []; |
| + this._filters = []; |
| + |
| + this._filterTextSet = new Set(); |
| + |
| if (title) |
| this._title = title; |
| + |
| Subscription.knownSubscriptions.set(url, this); |
| } |
| exports.Subscription = Subscription; |
| Subscription.prototype = |
| { |
| /** |
| * Download location of the subscription |
| @@ -53,20 +59,36 @@ |
| /** |
| * Type of the subscription |
| * @type {?string} |
| */ |
| type: null, |
| /** |
| - * Filters contained in the filter subscription |
| - * @type {Filter[]} |
| + * Filter text contained in the filter subscription. |
| + * @type {Array.<string>} |
| + * @private |
| */ |
| - filters: null, |
| + _filterText: null, |
| + |
| + /** |
| + * {@link Filter} objects corresponding to the subscription's filter text. |
| + * @type {Array.<Filter>} |
| + * @private |
| + */ |
| + _filters: null, |
| + |
| + /** |
| + * Set of filter text contained in the filter subscription, used for faster |
| + * lookup. |
| + * @type {Set.<string>} |
| + * @private |
| + */ |
| + _filterTextSet: null, |
| _title: null, |
| _fixedTitle: false, |
| _disabled: false, |
| /** |
| * Title of the filter subscription |
| * @type {string} |
| @@ -120,16 +142,138 @@ |
| let oldValue = this._disabled; |
| this._disabled = value; |
| filterNotifier.emit("subscription.disabled", this, value, oldValue); |
| } |
| return this._disabled; |
| }, |
| /** |
| + * The number of filters in the subscription. |
| + * @type {number} |
| + */ |
| + get filterCount() |
| + { |
| + return this._filters.length; |
| + }, |
| + |
| + /** |
| + * Yields the text for each filter in the subscription. |
| + * @yields {string} |
| + */ |
| + *filterText() |
| + { |
| + yield* this._filterText; |
| + }, |
| + |
| + /** |
| + * Yields the {@link Filter} object for each filter in the subscription. |
| + * @yields {Filter} |
| + */ |
| + *filters() |
| + { |
| + yield* this._filters; |
| + }, |
| + |
| + /** |
| + * Returns the {@link Filter} object at the given 0-based index. |
| + * @param {number} index |
| + * @returns {?Filter} |
| + */ |
| + filterAt(index) |
| + { |
| + return this._filters[index] || null; |
| + }, |
| + |
| + /** |
| + * Returns the 0-based index of the given filter. |
| + * @param {Filter} filter |
| + * @param {number} [fromIndex] The index from which to start the search. |
| + * @return {number} |
| + */ |
| + searchFilter(filter, fromIndex = 0) |
| + { |
| + return this._filterText.indexOf(filter.text, fromIndex); |
| + }, |
| + |
| + /** |
| + * Checks whether the subscription contains the given filter. |
| + * @param {Filter} filter |
| + * @return {boolean} |
| + */ |
| + hasFilter(filter) |
| + { |
| + return this._filterTextSet.has(filter.text); |
| + }, |
| + |
| + /** |
| + * Removes all filters from the subscription. |
| + */ |
| + clearFilters() |
| + { |
| + this._filterText = []; |
| + this._filters = []; |
| + |
| + this._filterTextSet.clear(); |
| + }, |
| + |
| + /** |
| + * Adds a filter to the subscription. |
| + * @param {Filter} filter |
| + */ |
| + addFilter(filter) |
| + { |
| + this._filterText.push(filter.text); |
| + this._filters.push(filter); |
| + |
| + this._filterTextSet.add(filter.text); |
| + }, |
| + |
| + /** |
| + * Inserts a filter into the subscription. |
| + * @param {Filter} filter |
| + * @param {number} index The index at which to insert the filter. |
| + */ |
| + insertFilterAt(filter, index) |
| + { |
| + this._filterText.splice(index, 0, filter.text); |
| + this._filters.splice(index, 0, filter); |
| + |
| + this._filterTextSet.add(filter.text); |
| + }, |
| + |
| + /** |
| + * Deletes a filter from the subscription. |
| + * @param {number} index The index at which to delete the filter. |
| + */ |
| + deleteFilterAt(index) |
| + { |
| + // Ignore index if out of bounds on the negative side, for consistency. |
| + if (index < 0) |
| + return; |
| + |
| + let items = this._filterText.splice(index, 1); |
| + |
| + this._filters.splice(index, 1); |
| + |
| + // Performance note: A subscription can contain the same filter multiple |
| + // times. We can only delete the text from the set if all occurrences of |
| + // the filter have been removed. This makes deletion an expensive |
| + // operation. It is only needed when a user-defined filter is removed, |
| + // which is relatively rare. In comparison, filters must be looked up in |
| + // subscriptions a lot more often; therefore, it makes sense to maintain |
| + // the set at this cost. An alternative here would be to use a map instead |
| + // of a set, with the value of each entry being the reference count, but |
| + // this would make addition more expensive thus slowing down the loading of |
| + // the initial subscriptions. |
| + if (items.length > 0 && this._filterText.indexOf(items[0]) == -1) |
| + this._filterTextSet.delete(items[0]); |
| + }, |
| + |
| + /** |
| * Serializes the subscription for writing out on disk. |
| * @yields {string} |
| */ |
| *serialize() |
| { |
| let {url, type, _title, _fixedTitle, _disabled} = this; |
| yield "[Subscription]"; |
| @@ -142,22 +286,22 @@ |
| if (_fixedTitle) |
| yield "fixedTitle=true"; |
| if (_disabled) |
| yield "disabled=true"; |
| }, |
| *serializeFilters() |
| { |
| - let {filters} = this; |
| + let {_filterText} = this; |
| yield "[Subscription filters]"; |
| - for (let filter of filters) |
| - yield filter.text.replace(/\[/g, "\\["); |
| + for (let text of _filterText) |
| + yield text.replace(/\[/g, "\\["); |
| }, |
| toString() |
| { |
| return [...this.serialize()].join("\n"); |
| } |
| }; |
| @@ -328,17 +472,17 @@ |
| * Creates a new user-defined filter group and adds the given filter to it. |
| * This group will act as the default group for this filter type. |
| * @param {Filter} filter |
| * @return {SpecialSubscription} |
| */ |
| SpecialSubscription.createForFilter = function(filter) |
| { |
| let subscription = SpecialSubscription.create(); |
| - subscription.filters.push(filter); |
| + subscription.addFilter(filter); |
| for (let [type, class_] of SpecialSubscription.defaultsMap) |
| { |
| if (filter instanceof class_) |
| subscription.defaults = [type]; |
| } |
| if (!subscription.defaults) |
| subscription.defaults = ["blocking"]; |
| return subscription; |