Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: lib/filterStorage.js

Issue 29335650: Issue 2595 - Use the core code from adblockpluscore (Closed)
Patch Set: Created Feb. 4, 2016, 6:35 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « lib/filterNotifier.js ('k') | lib/matcher.js » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 /*
2 * This file is part of Adblock Plus <https://adblockplus.org/>,
3 * Copyright (C) 2006-2016 Eyeo GmbH
4 *
5 * Adblock Plus is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License version 3 as
7 * published by the Free Software Foundation.
8 *
9 * Adblock Plus is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 /**
19 * @fileOverview FilterStorage class responsible for managing user's subscriptio ns and filters.
20 */
21
22 Cu.import("resource://gre/modules/Services.jsm");
23 Cu.import("resource://gre/modules/FileUtils.jsm");
24 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
25
26 let {IO} = require("io");
27 let {Prefs} = require("prefs");
28 let {Filter, ActiveFilter} = require("filterClasses");
29 let {Subscription, SpecialSubscription, ExternalSubscription} = require("subscri ptionClasses");
30 let {FilterNotifier} = require("filterNotifier");
31 let {Utils} = require("utils");
32
33 /**
34 * Version number of the filter storage file format.
35 * @type Integer
36 */
37 let formatVersion = 4;
38
39 /**
40 * This class reads user's filters from disk, manages them in memory and writes them back.
41 * @class
42 */
43 let FilterStorage = exports.FilterStorage =
44 {
45 /**
46 * Version number of the patterns.ini format used.
47 * @type Integer
48 */
49 get formatVersion()
50 {
51 return formatVersion;
52 },
53
54 /**
55 * File that the filter list has been loaded from and should be saved to
56 * @type nsIFile
57 */
58 get sourceFile()
59 {
60 let file = null;
61 if (Prefs.patternsfile)
62 {
63 // Override in place, use it instead of placing the file in the regular da ta dir
64 file = IO.resolveFilePath(Prefs.patternsfile);
65 }
66 if (!file)
67 {
68 // Place the file in the data dir
69 file = IO.resolveFilePath(Prefs.data_directory);
70 if (file)
71 file.append("patterns.ini");
72 }
73 if (!file)
74 {
75 // Data directory pref misconfigured? Try the default value
76 try
77 {
78 file = IO.resolveFilePath(Services.prefs.getDefaultBranch("extensions.ad blockplus.").getCharPref("data_directory"));
79 if (file)
80 file.append("patterns.ini");
81 } catch(e) {}
82 }
83
84 if (!file)
85 Cu.reportError("Adblock Plus: Failed to resolve filter file location from extensions.adblockplus.patternsfile preference");
86
87 // Property is configurable because of the test suite.
88 Object.defineProperty(this, "sourceFile", {value: file, configurable: true}) ;
89 return file;
90 },
91
92 /**
93 * Will be set to true if no patterns.ini file exists.
94 * @type Boolean
95 */
96 firstRun: false,
97
98 /**
99 * Map of properties listed in the filter storage file before the sections
100 * start. Right now this should be only the format version.
101 */
102 fileProperties: Object.create(null),
103
104 /**
105 * List of filter subscriptions containing all filters
106 * @type Subscription[]
107 */
108 subscriptions: [],
109
110 /**
111 * Map of subscriptions already on the list, by their URL/identifier
112 * @type Object
113 */
114 knownSubscriptions: Object.create(null),
115
116 /**
117 * Finds the filter group that a filter should be added to by default. Will
118 * return null if this group doesn't exist yet.
119 */
120 getGroupForFilter: function(/**Filter*/ filter) /**SpecialSubscription*/
121 {
122 let generalSubscription = null;
123 for (let subscription of FilterStorage.subscriptions)
124 {
125 if (subscription instanceof SpecialSubscription && !subscription.disabled)
126 {
127 // Always prefer specialized subscriptions
128 if (subscription.isDefaultFor(filter))
129 return subscription;
130
131 // If this is a general subscription - store it as fallback
132 if (!generalSubscription && (!subscription.defaults || !subscription.def aults.length))
133 generalSubscription = subscription;
134 }
135 }
136 return generalSubscription;
137 },
138
139 /**
140 * Adds a filter subscription to the list
141 * @param {Subscription} subscription filter subscription to be added
142 * @param {Boolean} silent if true, no listeners will be triggered (to be use d when filter list is reloaded)
143 */
144 addSubscription: function(subscription, silent)
145 {
146 if (subscription.url in FilterStorage.knownSubscriptions)
147 return;
148
149 FilterStorage.subscriptions.push(subscription);
150 FilterStorage.knownSubscriptions[subscription.url] = subscription;
151 addSubscriptionFilters(subscription);
152
153 if (!silent)
154 FilterNotifier.triggerListeners("subscription.added", subscription);
155 },
156
157 /**
158 * Removes a filter subscription from the list
159 * @param {Subscription} subscription filter subscription to be removed
160 * @param {Boolean} silent if true, no listeners will be triggered (to be use d when filter list is reloaded)
161 */
162 removeSubscription: function(subscription, silent)
163 {
164 for (let i = 0; i < FilterStorage.subscriptions.length; i++)
165 {
166 if (FilterStorage.subscriptions[i].url == subscription.url)
167 {
168 removeSubscriptionFilters(subscription);
169
170 FilterStorage.subscriptions.splice(i--, 1);
171 delete FilterStorage.knownSubscriptions[subscription.url];
172 if (!silent)
173 FilterNotifier.triggerListeners("subscription.removed", subscription);
174 return;
175 }
176 }
177 },
178
179 /**
180 * Moves a subscription in the list to a new position.
181 * @param {Subscription} subscription filter subscription to be moved
182 * @param {Subscription} [insertBefore] filter subscription to insert before
183 * (if omitted the subscription will be put at the end of the list)
184 */
185 moveSubscription: function(subscription, insertBefore)
186 {
187 let currentPos = FilterStorage.subscriptions.indexOf(subscription);
188 if (currentPos < 0)
189 return;
190
191 let newPos = insertBefore ? FilterStorage.subscriptions.indexOf(insertBefore ) : -1;
192 if (newPos < 0)
193 newPos = FilterStorage.subscriptions.length;
194
195 if (currentPos < newPos)
196 newPos--;
197 if (currentPos == newPos)
198 return;
199
200 FilterStorage.subscriptions.splice(currentPos, 1);
201 FilterStorage.subscriptions.splice(newPos, 0, subscription);
202 FilterNotifier.triggerListeners("subscription.moved", subscription);
203 },
204
205 /**
206 * Replaces the list of filters in a subscription by a new list
207 * @param {Subscription} subscription filter subscription to be updated
208 * @param {Filter[]} filters new filter list
209 */
210 updateSubscriptionFilters: function(subscription, filters)
211 {
212 removeSubscriptionFilters(subscription);
213 subscription.oldFilters = subscription.filters;
214 subscription.filters = filters;
215 addSubscriptionFilters(subscription);
216 FilterNotifier.triggerListeners("subscription.updated", subscription);
217 delete subscription.oldFilters;
218 },
219
220 /**
221 * Adds a user-defined filter to the list
222 * @param {Filter} filter
223 * @param {SpecialSubscription} [subscription] particular group that the filte r should be added to
224 * @param {Integer} [position] position within the subscription at which the f ilter should be added
225 * @param {Boolean} silent if true, no listeners will be triggered (to be use d when filter list is reloaded)
226 */
227 addFilter: function(filter, subscription, position, silent)
228 {
229 if (!subscription)
230 {
231 if (filter.subscriptions.some(s => s instanceof SpecialSubscription && !s. disabled))
232 return; // No need to add
233 subscription = FilterStorage.getGroupForFilter(filter);
234 }
235 if (!subscription)
236 {
237 // No group for this filter exists, create one
238 subscription = SpecialSubscription.createForFilter(filter);
239 this.addSubscription(subscription);
240 return;
241 }
242
243 if (typeof position == "undefined")
244 position = subscription.filters.length;
245
246 if (filter.subscriptions.indexOf(subscription) < 0)
247 filter.subscriptions.push(subscription);
248 subscription.filters.splice(position, 0, filter);
249 if (!silent)
250 FilterNotifier.triggerListeners("filter.added", filter, subscription, posi tion);
251 },
252
253 /**
254 * Removes a user-defined filter from the list
255 * @param {Filter} filter
256 * @param {SpecialSubscription} [subscription] a particular filter group that
257 * the filter should be removed from (if ommited will be removed from all subscriptions)
258 * @param {Integer} [position] position inside the filter group at which the
259 * filter should be removed (if ommited all instances will be removed)
260 */
261 removeFilter: function(filter, subscription, position)
262 {
263 let subscriptions = (subscription ? [subscription] : filter.subscriptions.sl ice());
264 for (let i = 0; i < subscriptions.length; i++)
265 {
266 let subscription = subscriptions[i];
267 if (subscription instanceof SpecialSubscription)
268 {
269 let positions = [];
270 if (typeof position == "undefined")
271 {
272 let index = -1;
273 do
274 {
275 index = subscription.filters.indexOf(filter, index + 1);
276 if (index >= 0)
277 positions.push(index);
278 } while (index >= 0);
279 }
280 else
281 positions.push(position);
282
283 for (let j = positions.length - 1; j >= 0; j--)
284 {
285 let position = positions[j];
286 if (subscription.filters[position] == filter)
287 {
288 subscription.filters.splice(position, 1);
289 if (subscription.filters.indexOf(filter) < 0)
290 {
291 let index = filter.subscriptions.indexOf(subscription);
292 if (index >= 0)
293 filter.subscriptions.splice(index, 1);
294 }
295 FilterNotifier.triggerListeners("filter.removed", filter, subscripti on, position);
296 }
297 }
298 }
299 }
300 },
301
302 /**
303 * Moves a user-defined filter to a new position
304 * @param {Filter} filter
305 * @param {SpecialSubscription} subscription filter group where the filter is located
306 * @param {Integer} oldPosition current position of the filter
307 * @param {Integer} newPosition new position of the filter
308 */
309 moveFilter: function(filter, subscription, oldPosition, newPosition)
310 {
311 if (!(subscription instanceof SpecialSubscription) || subscription.filters[o ldPosition] != filter)
312 return;
313
314 newPosition = Math.min(Math.max(newPosition, 0), subscription.filters.length - 1);
315 if (oldPosition == newPosition)
316 return;
317
318 subscription.filters.splice(oldPosition, 1);
319 subscription.filters.splice(newPosition, 0, filter);
320 FilterNotifier.triggerListeners("filter.moved", filter, subscription, oldPos ition, newPosition);
321 },
322
323 /**
324 * Increases the hit count for a filter by one
325 * @param {Filter} filter
326 */
327 increaseHitCount: function(filter)
328 {
329 if (!Prefs.savestats || !(filter instanceof ActiveFilter))
330 return;
331
332 filter.hitCount++;
333 filter.lastHit = Date.now();
334 },
335
336 /**
337 * Resets hit count for some filters
338 * @param {Filter[]} filters filters to be reset, if null all filters will be reset
339 */
340 resetHitCounts: function(filters)
341 {
342 if (!filters)
343 {
344 filters = [];
345 for (let text in Filter.knownFilters)
346 filters.push(Filter.knownFilters[text]);
347 }
348 for (let filter of filters)
349 {
350 filter.hitCount = 0;
351 filter.lastHit = 0;
352 }
353 },
354
355 _loading: false,
356
357 /**
358 * Loads all subscriptions from the disk
359 * @param {nsIFile} [sourceFile] File to read from
360 */
361 loadFromDisk: function(sourceFile)
362 {
363 if (this._loading)
364 return;
365
366 this._loading = true;
367
368 let readFile = function(sourceFile, backupIndex)
369 {
370 let parser = new INIParser();
371 IO.readFromFile(sourceFile, parser, function(e)
372 {
373 if (!e && parser.subscriptions.length == 0)
374 {
375 // No filter subscriptions in the file, this isn't right.
376 e = new Error("No data in the file");
377 }
378
379 if (e)
380 Cu.reportError(e);
381
382 if (e && !explicitFile)
383 {
384 // Attempt to load a backup
385 sourceFile = this.sourceFile;
386 if (sourceFile)
387 {
388 let [, part1, part2] = /^(.*)(\.\w+)$/.exec(sourceFile.leafName) || [null, sourceFile.leafName, ""];
389
390 sourceFile = sourceFile.clone();
391 sourceFile.leafName = part1 + "-backup" + (++backupIndex) + part2;
392
393 IO.statFile(sourceFile, function(e, statData)
394 {
395 if (!e && statData.exists)
396 readFile(sourceFile, backupIndex);
397 else
398 doneReading(parser);
399 });
400 return;
401 }
402 }
403 doneReading(parser);
404 }.bind(this));
405 }.bind(this);
406
407 var doneReading = function(parser)
408 {
409 // Old special groups might have been converted, remove them if they are e mpty
410 let specialMap = {"~il~": true, "~wl~": true, "~fl~": true, "~eh~": true};
411 let knownSubscriptions = Object.create(null);
412 for (let i = 0; i < parser.subscriptions.length; i++)
413 {
414 let subscription = parser.subscriptions[i];
415 if (subscription instanceof SpecialSubscription && subscription.filters. length == 0 && subscription.url in specialMap)
416 parser.subscriptions.splice(i--, 1);
417 else
418 knownSubscriptions[subscription.url] = subscription;
419 }
420
421 this.fileProperties = parser.fileProperties;
422 this.subscriptions = parser.subscriptions;
423 this.knownSubscriptions = knownSubscriptions;
424 Filter.knownFilters = parser.knownFilters;
425 Subscription.knownSubscriptions = parser.knownSubscriptions;
426
427 if (parser.userFilters)
428 {
429 for (let i = 0; i < parser.userFilters.length; i++)
430 {
431 let filter = Filter.fromText(parser.userFilters[i]);
432 this.addFilter(filter, null, undefined, true);
433 }
434 }
435
436 this._loading = false;
437 FilterNotifier.triggerListeners("load");
438
439 if (sourceFile != this.sourceFile)
440 this.saveToDisk();
441
442 }.bind(this);
443
444 let explicitFile;
445 if (sourceFile)
446 {
447 explicitFile = true;
448 readFile(sourceFile, 0);
449 }
450 else
451 {
452 explicitFile = false;
453 sourceFile = FilterStorage.sourceFile;
454
455 let callback = function(e, statData)
456 {
457 if (e || !statData.exists)
458 {
459 this.firstRun = true;
460 this._loading = false;
461 FilterNotifier.triggerListeners("load");
462 }
463 else
464 readFile(sourceFile, 0);
465 }.bind(this);
466
467 if (sourceFile)
468 IO.statFile(sourceFile, callback);
469 else
470 callback(true);
471 }
472 },
473
474 _generateFilterData: function*(subscriptions)
475 {
476 yield "# Adblock Plus preferences";
477 yield "version=" + formatVersion;
478
479 let saved = Object.create(null);
480 let buf = [];
481
482 // Save filter data
483 for (let i = 0; i < subscriptions.length; i++)
484 {
485 let subscription = subscriptions[i];
486 for (let j = 0; j < subscription.filters.length; j++)
487 {
488 let filter = subscription.filters[j];
489 if (!(filter.text in saved))
490 {
491 filter.serialize(buf);
492 saved[filter.text] = filter;
493 for (let k = 0; k < buf.length; k++)
494 yield buf[k];
495 buf.splice(0);
496 }
497 }
498 }
499
500 // Save subscriptions
501 for (let i = 0; i < subscriptions.length; i++)
502 {
503 let subscription = subscriptions[i];
504
505 yield "";
506
507 subscription.serialize(buf);
508 if (subscription.filters.length)
509 {
510 buf.push("", "[Subscription filters]")
511 subscription.serializeFilters(buf);
512 }
513 for (let k = 0; k < buf.length; k++)
514 yield buf[k];
515 buf.splice(0);
516 }
517 },
518
519 /**
520 * Will be set to true if saveToDisk() is running (reentrance protection).
521 * @type Boolean
522 */
523 _saving: false,
524
525 /**
526 * Will be set to true if a saveToDisk() call arrives while saveToDisk() is
527 * already running (delayed execution).
528 * @type Boolean
529 */
530 _needsSave: false,
531
532 /**
533 * Saves all subscriptions back to disk
534 * @param {nsIFile} [targetFile] File to be written
535 */
536 saveToDisk: function(targetFile)
537 {
538 let explicitFile = true;
539 if (!targetFile)
540 {
541 targetFile = FilterStorage.sourceFile;
542 explicitFile = false;
543 }
544 if (!targetFile)
545 return;
546
547 if (!explicitFile && this._saving)
548 {
549 this._needsSave = true;
550 return;
551 }
552
553 // Make sure the file's parent directory exists
554 try {
555 targetFile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECT ORY);
556 } catch (e) {}
557
558 let writeFilters = function()
559 {
560 IO.writeToFile(targetFile, this._generateFilterData(subscriptions), functi on(e)
561 {
562 if (!explicitFile)
563 this._saving = false;
564
565 if (e)
566 Cu.reportError(e);
567
568 if (!explicitFile && this._needsSave)
569 {
570 this._needsSave = false;
571 this.saveToDisk();
572 }
573 else
574 FilterNotifier.triggerListeners("save");
575 }.bind(this));
576 }.bind(this);
577
578 let checkBackupRequired = function(callbackNotRequired, callbackRequired)
579 {
580 if (explicitFile || Prefs.patternsbackups <= 0)
581 callbackNotRequired();
582 else
583 {
584 IO.statFile(targetFile, function(e, statData)
585 {
586 if (e || !statData.exists)
587 callbackNotRequired();
588 else
589 {
590 let [, part1, part2] = /^(.*)(\.\w+)$/.exec(targetFile.leafName) || [null, targetFile.leafName, ""];
591 let newestBackup = targetFile.clone();
592 newestBackup.leafName = part1 + "-backup1" + part2;
593 IO.statFile(newestBackup, function(e, statData)
594 {
595 if (!e && (!statData.exists || (Date.now() - statData.lastModified ) / 3600000 >= Prefs.patternsbackupinterval))
596 callbackRequired(part1, part2)
597 else
598 callbackNotRequired();
599 });
600 }
601 });
602 }
603 }.bind(this);
604
605 let removeLastBackup = function(part1, part2)
606 {
607 let file = targetFile.clone();
608 file.leafName = part1 + "-backup" + Prefs.patternsbackups + part2;
609 IO.removeFile(file, (e) => renameBackup(part1, part2, Prefs.patternsbackup s - 1));
610 }.bind(this);
611
612 let renameBackup = function(part1, part2, index)
613 {
614 if (index > 0)
615 {
616 let fromFile = targetFile.clone();
617 fromFile.leafName = part1 + "-backup" + index + part2;
618
619 let toName = part1 + "-backup" + (index + 1) + part2;
620
621 IO.renameFile(fromFile, toName, (e) => renameBackup(part1, part2, index - 1));
622 }
623 else
624 {
625 let toFile = targetFile.clone();
626 toFile.leafName = part1 + "-backup" + (index + 1) + part2;
627
628 IO.copyFile(targetFile, toFile, writeFilters);
629 }
630 }.bind(this);
631
632 // Do not persist external subscriptions
633 let subscriptions = this.subscriptions.filter((s) => !(s instanceof External Subscription));
634 if (!explicitFile)
635 this._saving = true;
636
637 checkBackupRequired(writeFilters, removeLastBackup);
638 },
639
640 /**
641 * Returns the list of existing backup files.
642 */
643 getBackupFiles: function() /**nsIFile[]*/
644 {
645 // TODO: This method should be asynchronous
646 let result = [];
647
648 let [, part1, part2] = /^(.*)(\.\w+)$/.exec(FilterStorage.sourceFile.leafNam e) || [null, FilterStorage.sourceFile.leafName, ""];
649 for (let i = 1; ; i++)
650 {
651 let file = FilterStorage.sourceFile.clone();
652 file.leafName = part1 + "-backup" + i + part2;
653 if (file.exists())
654 result.push(file);
655 else
656 break;
657 }
658 return result;
659 }
660 };
661
662 /**
663 * Joins subscription's filters to the subscription without any notifications.
664 * @param {Subscription} subscription filter subscription that should be connect ed to its filters
665 */
666 function addSubscriptionFilters(subscription)
667 {
668 if (!(subscription.url in FilterStorage.knownSubscriptions))
669 return;
670
671 for (let filter of subscription.filters)
672 filter.subscriptions.push(subscription);
673 }
674
675 /**
676 * Removes subscription's filters from the subscription without any notification s.
677 * @param {Subscription} subscription filter subscription to be removed
678 */
679 function removeSubscriptionFilters(subscription)
680 {
681 if (!(subscription.url in FilterStorage.knownSubscriptions))
682 return;
683
684 for (let filter of subscription.filters)
685 {
686 let i = filter.subscriptions.indexOf(subscription);
687 if (i >= 0)
688 filter.subscriptions.splice(i, 1);
689 }
690 }
691
692 /**
693 * IO.readFromFile() listener to parse filter data.
694 * @constructor
695 */
696 function INIParser()
697 {
698 this.fileProperties = this.curObj = {};
699 this.subscriptions = [];
700 this.knownFilters = Object.create(null);
701 this.knownSubscriptions = Object.create(null);
702 }
703 INIParser.prototype =
704 {
705 linesProcessed: 0,
706 subscriptions: null,
707 knownFilters: null,
708 knownSubscriptions : null,
709 wantObj: true,
710 fileProperties: null,
711 curObj: null,
712 curSection: null,
713 userFilters: null,
714
715 process: function(val)
716 {
717 let origKnownFilters = Filter.knownFilters;
718 Filter.knownFilters = this.knownFilters;
719 let origKnownSubscriptions = Subscription.knownSubscriptions;
720 Subscription.knownSubscriptions = this.knownSubscriptions;
721 let match;
722 try
723 {
724 if (this.wantObj === true && (match = /^(\w+)=(.*)$/.exec(val)))
725 this.curObj[match[1]] = match[2];
726 else if (val === null || (match = /^\s*\[(.+)\]\s*$/.exec(val)))
727 {
728 if (this.curObj)
729 {
730 // Process current object before going to next section
731 switch (this.curSection)
732 {
733 case "filter":
734 case "pattern":
735 if ("text" in this.curObj)
736 Filter.fromObject(this.curObj);
737 break;
738 case "subscription":
739 let subscription = Subscription.fromObject(this.curObj);
740 if (subscription)
741 this.subscriptions.push(subscription);
742 break;
743 case "subscription filters":
744 case "subscription patterns":
745 if (this.subscriptions.length)
746 {
747 let subscription = this.subscriptions[this.subscriptions.length - 1];
748 for (let text of this.curObj)
749 {
750 let filter = Filter.fromText(text);
751 subscription.filters.push(filter);
752 filter.subscriptions.push(subscription);
753 }
754 }
755 break;
756 case "user patterns":
757 this.userFilters = this.curObj;
758 break;
759 }
760 }
761
762 if (val === null)
763 return;
764
765 this.curSection = match[1].toLowerCase();
766 switch (this.curSection)
767 {
768 case "filter":
769 case "pattern":
770 case "subscription":
771 this.wantObj = true;
772 this.curObj = {};
773 break;
774 case "subscription filters":
775 case "subscription patterns":
776 case "user patterns":
777 this.wantObj = false;
778 this.curObj = [];
779 break;
780 default:
781 this.wantObj = undefined;
782 this.curObj = null;
783 }
784 }
785 else if (this.wantObj === false && val)
786 this.curObj.push(val.replace(/\\\[/g, "["));
787 }
788 finally
789 {
790 Filter.knownFilters = origKnownFilters;
791 Subscription.knownSubscriptions = origKnownSubscriptions;
792 }
793
794 // Allow events to be processed every now and then.
795 // Note: IO.readFromFile() will deal with the potential reentrance here.
796 this.linesProcessed++;
797 if (this.linesProcessed % 1000 == 0)
798 Utils.yield();
799 }
800 };
OLDNEW
« no previous file with comments | « lib/filterNotifier.js ('k') | lib/matcher.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld