/** * Copyright 2018-present Facebook. * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * @format */ import type {Tracker, RulesToClass} from './index.js'; import type {StyleSheet} from './sheet.js'; const invariant = require('invariant'); export class GarbageCollector { constructor(sheet: StyleSheet, tracker: Tracker, rulesToClass: RulesToClass) { this.sheet = sheet; this.tracker = tracker; // used to keep track of what classes are actively in use this.usedClasses = new Map(); // classes to be removed, we put this in a queue and perform it in bulk rather than straight away // since by the time the next tick happens this style could have been reinserted this.classRemovalQueue = new Set(); this.rulesToClass = rulesToClass; } tracker: Tracker; sheet: StyleSheet; usedClasses: Map; garbageTimer: ?TimeoutID; classRemovalQueue: Set; rulesToClass: RulesToClass; hasQueuedCollection(): boolean { return Boolean(this.garbageTimer); } getReferenceCount(key: string): number { return this.usedClasses.get(key) || 0; } // component has been mounted so make sure it's being depended on registerClassUse(name: string) { const count = this.usedClasses.get(name) || 0; this.usedClasses.set(name, count + 1); if (this.classRemovalQueue.has(name)) { this.classRemovalQueue.delete(name); if (this.classRemovalQueue.size === 0) { this.haltGarbage(); } } } // component has been unmounted so remove it's dependencies deregisterClassUse(name: string) { let count = this.usedClasses.get(name); if (count == null) { return; } count--; this.usedClasses.set(name, count); if (count === 0) { this.classRemovalQueue.add(name); this.scheduleGarbage(); } } scheduleGarbage() { if (this.garbageTimer != null) { return; } this.garbageTimer = setTimeout(() => { this.collectGarbage(); }, 2000); } haltGarbage() { if (this.garbageTimer) { clearTimeout(this.garbageTimer); this.garbageTimer = null; } } getCollectionQueue() { return Array.from(this.classRemovalQueue); } collectGarbage() { this.haltGarbage(); for (const name of this.classRemovalQueue) { const trackerInfo = this.tracker.get(name); invariant(trackerInfo != null, 'trying to remove unknown class'); const {rules} = trackerInfo; this.rulesToClass.delete(rules); this.sheet.delete(name); this.tracker.delete(name); this.usedClasses.delete(name); } this.classRemovalQueue.clear(); } flush() { this.haltGarbage(); this.classRemovalQueue.clear(); this.usedClasses.clear(); } }