Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Build

on:
pull_request:
push:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: src

steps:
- name: Checkout
uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
cache-dependency-path: src/package-lock.json #This path is relative to the repo root, not the working-directory

- name: Install dependencies
run: npm ci

- name: Build
run: npm run build
104 changes: 60 additions & 44 deletions src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,34 +146,16 @@ async function inspectUrl(tab: browser.Tabs.Tab, changeInfo: browser.Tabs.OnUpda
Logger.logDebug(`${matchedBy} '${matchedPattern}' matched pattern '${pattern}'. Scheduling tab to be closed in ${timeout}ms`);

setTimeout(async () => {
let wasClosed = false;
try {
wasClosed = await closeTheTab(tabId!, periodicSettingSyncer.dontCloseLastTab);
if (wasClosed) {
saveHit();
}
} finally {
if (wasClosed) {
Logger.logDebug(`Scheduled tab closing for ${matchedBy} '${matchedPattern}' that matched pattern '${pattern}' after ${timeout}ms`);
} else {
Logger.logDebug(`Scheduled tab was already closed for ${matchedBy} '${matchedPattern}' that matched pattern '${pattern}' after ${timeout}ms`);
}
}
await attemptTabClose(
tabId!, periodicSettingSyncer.dontCloseLastTab, matchedBy, matchedPattern, pattern, saveHit,
`Scheduled tab closing for ${matchedBy} '${matchedPattern}' that matched pattern '${pattern}' after ${timeout}ms`
);
}, timeout);
} else { // close the tab immediately
let wasClosed = false;
try {
wasClosed = await closeTheTab(tabId!, periodicSettingSyncer.dontCloseLastTab);
if (wasClosed) {
saveHit();
}
} finally {
if (wasClosed) {
Logger.logDebug(`${matchedBy} '${matchedPattern}' matched pattern '${pattern}'. Tab has been closed.`);
} else {
Logger.logDebug(`${matchedBy} '${matchedPattern}' matched pattern '${pattern}'. But the tab was already closed.`);
}
}
await attemptTabClose(
tabId!, periodicSettingSyncer.dontCloseLastTab, matchedBy, matchedPattern, pattern, saveHit,
`${matchedBy} '${matchedPattern}' matched pattern '${pattern}'. Tab has been closed.`
);
}

return; //we're done!
Expand All @@ -190,42 +172,76 @@ async function inspectUrl(tab: browser.Tabs.Tab, changeInfo: browser.Tabs.OnUpda
}
} catch (error: any) {
const errorMessage = error?.message || "";
Logger.logError(`Something went wrong while processing url '${tabUrl}' with title '${tabTitle}': ${errorMessage}`);
}
Comment thread
mukunku marked this conversation as resolved.
}

//Tab might not exist if it matched by both Url and Title since one will be faster to close the tab before the other
if (!errorMessage.startsWith("No tab with id:") /*Chrome*/ && !errorMessage.startsWith("Invalid tab ID:") /*Firefox*/) {
Logger.logError(`Something went wrong while processing url '${tabUrl}' with title '${tabTitle}': ${error.message}`);
async function attemptTabClose(
tabId: number,
dontCloseLastTab: boolean,
matchedBy: string,
matchedPattern: string,
pattern: string,
saveHit: () => void,
successMessage: string
): Promise<void> {
let wasClosed = false;
try {
wasClosed = await closeTheTab(tabId, dontCloseLastTab);
if (wasClosed) {
saveHit();
}
} catch (error: any) {
if (IsTabDoesNotExistError(error)) {
Logger.logTrace(`Tab ${tabId} was already closed for ${matchedBy} '${matchedPattern}' that matched pattern '${pattern}'`);
} else {
Logger.logTrace(`Tab with url '${tabUrl}' and title '${tabTitle}' was already closed`)
const errorMessage = error?.message || "";
Logger.logError(`Something went wrong while closing tab ${tabId} for ${matchedBy} '${matchedPattern}' that matched pattern '${pattern}': ${errorMessage}`);
}
} finally {
if (wasClosed) {
Logger.logDebug(successMessage);
}
}
}

//Distinguishes "tab was already closed by another call" (expected, benign) from real failures
// E.g. Tab might not exist if it matched by both Url and Title since one will be faster to close the tab before the other
function IsTabDoesNotExistError(error: any): boolean {
const errorMessage = error?.message || "";
return errorMessage.startsWith("No tab with id:") /*Chrome*/ || errorMessage.startsWith("Invalid tab ID:") /*Firefox*/;
}

async function closeTheTab(tabId: number, dontCloseLastTab: boolean): Promise<boolean> {
//We need exclusive access to tabs so hit statistics are accurate. This is because
//we now allow matching by title and url so if both match that counts as two hits without locking.
const release = await (await acquireTabLock(tabId)).acquire();
try {
//check if this is the only tab
const tabsPromise = browser.tabs.query({ windowType: 'normal' });

if (dontCloseLastTab && (await tabsPromise).length === 1) {
//lets open a blank tab before closing the last one
await browser.tabs.create({ url: "about:blank" });
if (dontCloseLastTab) {
const tabs = await tabsPromise;
Comment thread
mukunku marked this conversation as resolved.

//tab may have already been closed by an earlier/concurrent scheduled or immediate close
if (!tabs.some(tab => tab.id === tabId)) {
Logger.logTrace(`Tab ${tabId} not found`);
return false;
}

if (tabs.length === 1) {
Logger.logTrace(`Tab ${tabId} is the only tab open. Creating a blank tab before closing it.`);

//lets open a blank tab before closing the last one
await browser.tabs.create({ url: "about:blank" });
}
}

//close first, ask questions later
Logger.logTrace(`Closing tab ${tabId}`);
await browser.tabs.remove(tabId);

//confirm we actually had a tab with that id to begin with (i.e. wasn't closed already). Not sure if this is needed or not.
if ((await tabsPromise).filter(tab => tab.id === tabId).length > 0) {
Logger.logTrace(`Tab ${tabId} closed successfully`);
return true;
} else {
//tab doesn't exist anymore. probably already closed by another rule
Logger.logTrace(`Tab ${tabId} not found`);
return false;
}
Logger.logTrace(`Tab ${tabId} closed successfully`);
return true;
} finally {
release();
}
Expand Down Expand Up @@ -266,7 +282,7 @@ browser.storage.onChanged.addListener(async (changes, namespace) => {
} else if (key === StorageApi.DONT_CLOSE_LAST_TAB_KEY) {
const logger = await Logger.getInstance();
let periodicSettingSyncer = await PeriodicSettingSyncer.getInstance(logger);
periodicSettingSyncer.dontCloseLastTab = newValue;
periodicSettingSyncer.dontCloseLastTab = newValue as boolean;
}

if (key?.startsWith("config-")) {
Expand Down
4 changes: 2 additions & 2 deletions src/helpers/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export enum RuntimeEnvironment {
}

export class Environment {
public static getEnvironment(): RuntimeEnvironment {
private static getEnvironment(): RuntimeEnvironment {
const webpackEnvironment: string = process.env.NODE_ENV || "";
if (webpackEnvironment === "development") {
return RuntimeEnvironment.Development;
Expand All @@ -27,7 +27,7 @@ export class Environment {
}

public static isFirefox(): boolean {
return !browser.storage.local.QUOTA_BYTES; //QUOTA_BYTES is not defined in FF
return !Object.prototype.hasOwnProperty.call(browser.storage.local, "QUOTA_BYTES"); //QUOTA_BYTES is undefined in Firefox
}
Comment thread
mukunku marked this conversation as resolved.

public static prefersDarkMode(): boolean {
Expand Down
4 changes: 2 additions & 2 deletions src/helpers/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ export class Logger {
private static mutex: Mutex = new Mutex();
private storage: LocalStorageApi;
private queue: Queue<LogRecord>;
private intervalId: NodeJS.Timeout;
private intervalId: ReturnType<typeof setInterval>;
private syncFailureCount: number = 0;
public minLogLevel: LogLevel;
public readonly readonly: boolean = false;

private constructor(readonly: boolean) {
this.readonly = readonly;
this.queue = new Queue<LogRecord>();
this.intervalId = 0 as unknown as NodeJS.Timeout
this.intervalId = 0 as unknown as ReturnType<typeof setInterval>
this.storage = new LocalStorageApi();

if (readonly) {
Expand Down
9 changes: 7 additions & 2 deletions src/manifest.firefox.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Tab Close Gold",
"version": "3.2.0.0",
"version": "3.2.1.0",
"permissions": [
"tabs",
"storage"
Expand Down Expand Up @@ -33,7 +33,12 @@
"browser_specific_settings": {
"gecko": {
"id": "tabclosegold@mukunku.com",
"strict_min_version": "109.0"
"strict_min_version": "109.0",
"data_collection_permissions": {
"required": [
"none"
]
}
}
}
}
2 changes: 1 addition & 1 deletion src/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Tab Close Gold",
"version": "3.2.0.0",
"version": "3.2.1.0",
"permissions": [
"tabs",
"storage"
Expand Down
Loading