Skip to content

Commit

Permalink
MRU caching initial implementation (#32)
Browse files Browse the repository at this point in the history
* Started MRU Caching by tab id

* Added tab openlistener to work with MRU

* Added MRU caching and tests. Works pretty good.

* renamed MRU tests to integration since they are larger scale

* doing some refactoring but broke tests

* Some small cleanup

* Made tests faster idk how

* Got switch tab to work by manually invoking

* Uses window-based dict

needs more tests
some race conditions

---------

Co-authored-by: sorendahl <[email protected]>
  • Loading branch information
Soreloser2 and sorendahl authored Feb 14, 2023
1 parent 8485e35 commit 3a6c0db
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 31 deletions.
97 changes: 67 additions & 30 deletions src/background.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,81 @@
// keep an mru cache for every window

// mru cache, not in use
var mruCache = [];
const DEBUG = true;

async function setMRU(tabs) {
const window = await chrome.windows.getCurrent();
const result = await chrome.storage.session.get("MRU_ID_Cache");
var dict = result.MRU_ID_Cache;
if (dict === undefined) {
dict = {};
}
dict[window.id] = tabs;
await chrome.storage.session.set({"MRU_ID_Cache": dict});
if (DEBUG) {
await logMRU();
}
}

async function initializeMRU(tabId, windowId) {
var dict = {};
dict[windowId] = [tabId];
await chrome.storage.session.set({"MRU_ID_Cache": dict});
return dict[windowId];
}

async function logMRU() {
const cache = await getMRU();
console.log(cache);
}

async function getMRU() {
const window = await chrome.windows.getCurrent();
const result = await chrome.storage.session.get("MRU_ID_Cache");
// if there isn't a cache already set, don't try to query that window
if (result.MRU_ID_Cache === undefined) {
return undefined;
}
return result.MRU_ID_Cache[window.id];
}

async function switch_tab() {
if (DEBUG) {
console.log("command triggered");
}
const cache = await getMRU();
// this automatically calls to the tab onActivated callback so we don't
// need to set the cache to anything here
await chrome.tabs.update(cache.at(cache.length - 2), {active: true, highlighted: true});
}

// Listen for keyboard shortcut
chrome.commands.onCommand.addListener(function(command) {
chrome.commands.onCommand.addListener(async function(command) {
// the 'switch-tab' command is defined in the manifest
if (command === "switch-tab") {
console.log("command triggered");

// we're looking within session data to grab the lastTabId
chrome.storage.session.get(["lastTabId"]).then((result) => {
console.log("Value currently is " + result.lastTabId);
// the update method grabs the tab it receives in the first parameter and
// makes its 'active' and 'highlight' features true, which acts to switch to it
// 'active' lets the tab be computationally active
// 'highlighted' does the actual switching to the tab
chrome.tabs.update(result.lastTabId, {active: true, highlighted: true});
});

await switch_tab();
}
});

// Listen for every new tab
chrome.tabs.onActivated.addListener(function(activeInfo) {
chrome.tabs.onActivated.addListener(async function(activeInfo) {
// need to keep session data stored. otherwise, data will get removed when the service worker
// gets shut down, which happens if another program is in focus within the device
var cache = await getMRU();
if (cache === undefined) {
// we haven't initialized a cache yet, need to add
// based on current tab and window
// the reason we don't do this on session/Chrome startup is
// because race conditions can still cause the session to get
// asked for the cache before the cache has been set from the startup handler\
cache = await initializeMRU(activeInfo.tabId, activeInfo.windowId);
}
console.log(cache);

// this gets the value of currTabId from session storage, and sets lastTabId to it
chrome.storage.session.get(["currTabId"]).then((result) => {
chrome.storage.session.set({ "lastTabId": result.currTabId }).then(() => {
console.log("lastTabId is set to " + result.currTabId);
});
});
// this set the value of currTabId to the current tab's id
chrome.storage.session.set({ "currTabId": activeInfo.tabId }).then(() => {
console.log("currTabId is set to " + activeInfo.tabId);
});

// TODO: should we implement an inverted index to have quick removing?

// mruCache.push(activeInfo.tabId);
const activeTab = activeInfo.tabId;
const index = cache.indexOf(activeTab);
if (index != -1) {
cache.splice(index, 1);
}
cache.push(activeTab);
await setMRU(cache);
});
78 changes: 78 additions & 0 deletions test/integration/test-switch-tab.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use strict'

import puppeteer from "puppeteer";
import test from "node:test";
import assert from "node:assert"
import {initializeExtension, sleep} from "../utils.js";

test("Top level extension test", async (t) => {
// can comment this out and just have
// var browser;
// but this line gives type hinting
var browser = await puppeteer.launch(); await browser.close();
var service_worker;
t.beforeEach(async (t) => {
[browser, service_worker] = await initializeExtension();
});
t.afterEach(async (t) => {
await browser.close();
});

await t.test("Test manual Initialization of MRU", async (t) => {
const cache = await service_worker.evaluate(async () => {
const [tab] = await chrome.tabs.query({"currentWindow": true});
const window = await chrome.windows.getCurrent();
await initializeMRU(tab.id, window.id);
return await getMRU();
});
assert.notDeepStrictEqual(cache, {}, "expected a real object result");
assert.notStrictEqual(cache, undefined, "expected a cache to be found");
assert.strictEqual(typeof(cache), typeof([]));
assert.strictEqual(cache.length, 1);
});

await t.test('Test Initialization of MRU by trigger', async (t) => {
const page = await browser.newPage();
await page.bringToFront();
const cache = await service_worker.evaluate(async () => {
return await getMRU();
});
assert.strictEqual(typeof(cache), typeof([]));
assert.strictEqual(cache.length, 1, "expect one tab currently in the cache");
});

await t.test('Test Initialization of MRU with 2 tabs', async (t) => {
const page1 = await browser.newPage();
await page1.bringToFront();
const page2 = await browser.newPage();
await page2.bringToFront();
const cache = await service_worker.evaluate(async () => {
return await getMRU();
});
assert.strictEqual(cache.length, 2, "expect two tabs currently in the cache");
});

// can't utilize keyboard shortcuts to test tab switching but can just call the method
await t.test('Test switch tabs', async (t) => {
const page1 = await browser.newPage();
await page1.bringToFront();
const page2 = await browser.newPage();
await page2.bringToFront();

const cache1 = await service_worker.evaluate(async () => {
return await getMRU();
});
// for some reason if we put this call in the block where we get cache2, it fails
// I think it's a timing thing and sending the eval request takes long enough
// so that the new tab listener triggers.
await service_worker.evaluate(async () => {
await switch_tab();
});
const cache2 = await service_worker.evaluate(async () => {
return await getMRU();
});
assert.strictEqual(cache1[0], cache2[1], "tabs should be swapped");
assert.strictEqual(cache2[0], cache1[1], "tabs should be swapped");
});
});

3 changes: 3 additions & 0 deletions test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "module"
}
2 changes: 1 addition & 1 deletion test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const assert = require('node:assert');

test('basic test', async (t) => {
assert.strictEqual(1 + 2, 3, 'expected 3');
})
});

test('test puppeteer javascript Eval', async (t) => {
const browser = await puppeteer.launch();
Expand Down
29 changes: 29 additions & 0 deletions test/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict'

import puppeteer from 'puppeteer';
import path from "path";

export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

export async function initializeExtension() {
const pathToExtension = path.join(process.cwd(), 'src');
const browser = await puppeteer.launch({
headless: false,
// uncomment for trying to actually see what puppeteer is doing
// or if there could be race conditions
// if there are race conditions, put a sleep in the code instead of leaving it on
//slowMo: 500,
args: [
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`,
],
});
const serviceWorkerTarget = await browser.waitForTarget(
target => target.type() === 'service_worker'
);
const service_worker = await serviceWorkerTarget.worker();
// need a small pause to allow chrome to set certain things
// I'm not sure why a pause of 0 ms works but without this line some tests fail
await sleep(0);
return [browser, service_worker];
}

0 comments on commit 3a6c0db

Please sign in to comment.