<commit_before>import assert from '../lib/assert.js';
import logError from '../lib/logError.js';

type RequireValues<T, K extends keyof T> = T & Required<Pick<T, K>>;

function requireValues<T, K extends keyof T>(obj: T, ...keys: K[]) {
	for (let k of keys) {
		if (obj[k] == null) {
			throw new Error(`Value for field ${k} is required`);
		}
	}
	return obj as RequireValues<T, K>;
}

export type TWindow = ReturnType<typeof convertWindow>;

function convertWindow(wndw: browser.windows.Window) {
	let { id, type, state, focused, tabs } = requireValues(wndw, 'id', 'type', 'state', 'tabs');
	assert(type === 'normal');
	assert(id !== browser.windows.WINDOW_ID_NONE);
	let ttabs: TTab[] = tabs.map(convertTab);
	// TODO: last focused
	return {
		id,
		state,
		focused,
		tabs: ttabs.toMap(t => t.id),
		activeTabId: ttabs.filter(t => t.active).single().id,
	};
}

export type TTab = ReturnType<typeof convertTab>;

function convertTab(tab: browser.tabs.Tab) {
	return requireValues(tab, 'id', 'url', 'discarded', 'status', 'title', 'windowId');
	// TODO: remove "active" property
}

type OnTabCreated = Parameters<typeof browser.tabs.onCreated.addListener>[0];
type OnTabRemoved = Parameters<typeof browser.tabs.onRemoved.addListener>[0];
type OnTabUpdated = Parameters<typeof browser.tabs.onUpdated.addListener>[0];
type OnTabActivated = Parameters<typeof browser.tabs.onActivated.addListener>[0];

export default class TabMirror {

	listeners = new Set<() => void>();
	private windows = new Map<number, TWindow>();
	private tabToWindowId = new Map<number, number>();

	constructor() {
		this.loadAll().then(() => {
			// Tab event handlers
			browser.tabs.onCreated.addListener(this.handleTabCreated);
			browser.tabs.onRemoved.addListener(this.handleTabRemoved);
			browser.tabs.onActivated.addListener(this.handleTabActivated);
			try {
				// Firefox is the only browser that currently supports filters
				browser.tabs.onUpdated.addListener(this.handleTabUpdate, { properties: ['title', 'status', 'favIconUrl', 'discarded'] });
			} catch {
				browser.tabs.onUpdated.addListener(this.handleTabUpdate);
			}
		}).catch(logError);
	}

	dispose() {
		browser.tabs.onCreated.removeListener(this.handleTabCreated);
		browser.tabs.onUpdated.removeListener(this.handleTabUpdate);
		browser.tabs.onActivated.removeListener(this.handleTabActivated);
	}

	private async loadAll() {
		let windows = (await browser.windows.getAll({ windowTypes: ['normal'], populate: true })).map(convertWindow);
		this.windows = windows.toMap(w => w.id);
		this.tabToWindowId = windows.flatMap(w => Array.from(w.tabs.values())).toMap(t => t.id, t => t.windowId);
		this.notify();
	}

	private handleTabCreated: OnTabCreated = x => {
		let tab = convertTab(x);
		this.windows.getOrThrow(tab.windowId).tabs.set(tab.id, tab);
		this.tabToWindowId.set(tab.id, tab.windowId);
		this.notify();
	};

	private handleTabRemoved: OnTabRemoved = (tabId, { windowId }) => {
		assert(this.windows.getOrThrow(windowId).tabs.delete(tabId));
		assert(this.tabToWindowId.delete(tabId));
		this.notify();
	};

	/**
	 * When the active tab in a window changes
	 */
	private handleTabActivated: OnTabActivated = ({ tabId, windowId }) => {
		let w = this.windows.get(windowId);
		if (w == null) {
			return; // Other windowType
		}
		// TODO: manually update lastAccessed
		// TODO: update selectedTabId
		w.activeTabId = tabId;
		this.notify();
	};

	/**
	 * Informs us whenever a tab property updates
	 */
	private handleTabUpdate: OnTabUpdated = (tabId, info, fullTab) => {
		console.log('updated', tabId, info);
		let windowId = this.tabToWindowId.get(tabId);
		if (windowId == null) {
			return; // Other windowType
		}
		let tab = this.windows.getOrThrow(windowId).tabs.getOrThrow(tabId);
		Object.assign(tab, info);
		tab.lastAccessed = fullTab.lastAccessed;
		this.notify();
	};

	private notify() {
		for (let listener of this.listeners) {
			listener();
		}
	}

	getWindows() {
		return this.windows.values();
	}
	
	findTab(tabId: number) {
		let windowId = this.tabToWindowId.getOrThrow(tabId);
		return this.windows.getOrThrow(windowId).tabs.getOrThrow(tabId);
	}
}<commit_msg>Use windowId from parameters<commit_after>import assert from '../lib/assert.js';
import logError from '../lib/logError.js';

type RequireValues<T, K extends keyof T> = T & Required<Pick<T, K>>;

function requireValues<T, K extends keyof T>(obj: T, ...keys: K[]) {
	for (let k of keys) {
		if (obj[k] == null) {
			throw new Error(`Value for field ${k} is required`);
		}
	}
	return obj as RequireValues<T, K>;
}

export type TWindow = ReturnType<typeof convertWindow>;

function convertWindow(wndw: browser.windows.Window) {
	let { id, type, state, focused, tabs } = requireValues(wndw, 'id', 'type', 'state', 'tabs');
	assert(type === 'normal');
	assert(id !== browser.windows.WINDOW_ID_NONE);
	let ttabs: TTab[] = tabs.map(convertTab);
	// TODO: last focused
	return {
		id,
		state,
		focused,
		tabs: ttabs.toMap(t => t.id),
		activeTabId: ttabs.filter(t => t.active).single().id,
	};
}

export type TTab = ReturnType<typeof convertTab>;

function convertTab(tab: browser.tabs.Tab) {
	return requireValues(tab, 'id', 'url', 'discarded', 'status', 'title', 'windowId');
	// TODO: remove "active" property
}

type OnTabCreated = Parameters<typeof browser.tabs.onCreated.addListener>[0];
type OnTabRemoved = Parameters<typeof browser.tabs.onRemoved.addListener>[0];
type OnTabUpdated = Parameters<typeof browser.tabs.onUpdated.addListener>[0];
type OnTabActivated = Parameters<typeof browser.tabs.onActivated.addListener>[0];

export default class TabMirror {

	listeners = new Set<() => void>();
	private windows = new Map<number, TWindow>();
	private tabToWindowId = new Map<number, number>();

	constructor() {
		this.loadAll().then(() => {
			// Tab event handlers
			browser.tabs.onCreated.addListener(this.handleTabCreated);
			browser.tabs.onRemoved.addListener(this.handleTabRemoved);
			browser.tabs.onActivated.addListener(this.handleTabActivated);
			try {
				// Firefox is the only browser that currently supports filters
				browser.tabs.onUpdated.addListener(this.handleTabUpdate, { properties: ['title', 'status', 'favIconUrl', 'discarded'] });
			} catch {
				browser.tabs.onUpdated.addListener(this.handleTabUpdate);
			}
		}).catch(logError);
	}

	dispose() {
		browser.tabs.onCreated.removeListener(this.handleTabCreated);
		browser.tabs.onUpdated.removeListener(this.handleTabUpdate);
		browser.tabs.onActivated.removeListener(this.handleTabActivated);
	}

	private async loadAll() {
		let windows = (await browser.windows.getAll({ windowTypes: ['normal'], populate: true })).map(convertWindow);
		this.windows = windows.toMap(w => w.id);
		this.tabToWindowId = windows.flatMap(w => Array.from(w.tabs.values())).toMap(t => t.id, t => t.windowId);
		this.notify();
	}

	private handleTabCreated: OnTabCreated = x => {
		let tab = convertTab(x);
		this.windows.getOrThrow(tab.windowId).tabs.set(tab.id, tab);
		this.tabToWindowId.set(tab.id, tab.windowId);
		this.notify();
	};

	private handleTabRemoved: OnTabRemoved = (tabId, { windowId }) => {
		assert(this.windows.getOrThrow(windowId).tabs.delete(tabId));
		assert(this.tabToWindowId.delete(tabId));
		this.notify();
	};

	/**
	 * When the active tab in a window changes
	 */
	private handleTabActivated: OnTabActivated = ({ tabId, windowId }) => {
		let w = this.windows.get(windowId);
		if (w == null) {
			return; // Other windowType
		}
		// TODO: manually update lastAccessed
		// TODO: update selectedTabId
		w.activeTabId = tabId;
		this.notify();
	};

	/**
	 * Informs us whenever a tab property updates
	 */
	private handleTabUpdate: OnTabUpdated = (tabId, info, fullTab) => {
		let w = this.windows.get(fullTab.windowId);
		if (w == null) {
			return; // Other windowType
		}
		let tab = w.tabs.getOrThrow(tabId);
		Object.assign(tab, info);
		tab.lastAccessed = fullTab.lastAccessed;
		this.notify();
	};

	private notify() {
		for (let listener of this.listeners) {
			listener();
		}
	}

	getWindows() {
		return this.windows.values();
	}
	
	findTab(tabId: number) {
		let windowId = this.tabToWindowId.getOrThrow(tabId);
		return this.windows.getOrThrow(windowId).tabs.getOrThrow(tabId);
	}
}
{'fraction_non_alphanumeric': 0.15417324747904265, 'fraction_numerical': 0.0009719353662981412, 'mean_word_length': 4.477045908183633, 'pattern_counts': {'":': 0, '<': 31, '<?xml version=': 0, '>': 55, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2084887', 'n_tokens_mistral': 2936, 'n_tokens_neox': 2721, 'n_words': 594}