New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

only-one-tab

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

only-one-tab - npm Package Compare versions

Comparing version 1.0.0 to 1.0.1

56

only-one-tab.js

@@ -44,6 +44,8 @@ /* global localStorage */

// localStorage.setItem(vacantKey, 'vacant') signals the actor tab closed
window.addEventListener('storage', async (event) => {
window.addEventListener('storage', async function handler (event) {
if (event.key === vacantKey && event.newValue === 'vacant') {
if (await race(actorRaceId)) {
becomeActor()
window.removeEventListener('storage', handler) // cleanup
}

@@ -53,24 +55,46 @@ }

// try to act initially
// if there's already an actor, the race will be lost
if (await race(actorRaceId)) {
becomeActor()
// reset if the last active tab closed without ending the actor race somehow
} else if (isTimedOut()) {
// multiple tabs may try to reset at once, so a race is necessary
if (await race(resetRaceId)) {
// no need to end the actorRace because this tab replaces the old winner
// check if this tab should become actor
// on start and then periodically to ensure nothing gets stuck
// sometimes the 'storage' event doesn't fire
// somtimes the actorRace randomly gets stuck
while (true) {
// actorRace lasts until actor tab is closed
if (await race(actorRaceId)) {
becomeActor()
// wait for heartbeat to start (no resets with active heartbeat)
// also wait for any other resetters to finish to prevent multiple actors
await sleep(1000)
endRace(resetRaceId)
break // end periodic check since we're the actor now
// reset if the last active tab closed without ending the actor race somehow
} else if (isTimedOut()) {
// multiple tabs may try to reset at once, so a race is necessary
if (await race(resetRaceId)) {
// no need to end the actorRace because this tab replaces the old winner
becomeActor()
// wait for heartbeat to start (no resets with active heartbeat)
// also wait for any other resetters to finish to prevent multiple actors
await sleep(1000)
endRace(resetRaceId)
break // end periodic check since we're the actor now
}
}
// indexedDB operations are expensive, and things only get stuck very rarely
await sleep(5 * 1000)
}
// returns a boolean for whether there is an active tab
// returns a boolean for whether an old actor tab crashed
// (closed without cleaning up and signalling for a new actor)
function isTimedOut () {
const lastHeartbeatString = localStorage.getItem(heartbeatKey)
// if the key is null, then there was no previous actor.
// not checking for this allows multiple actors when multiple tabs are
// opened before the first actor makes a heartbeat
// one wins the race and another wins the unnecessary reset
if (lastHeartbeatString === null) {
return false
}
const msSinceLastHeartbeat = new Date() - new Date(lastHeartbeatString)

@@ -77,0 +101,0 @@

{
"name": "only-one-tab",
"version": "1.0.0",
"version": "1.0.1",
"description": "Run a function in exactly one open tab, switching to another if closed",

@@ -5,0 +5,0 @@ "main": "only-one-tab.js",

@@ -11,2 +11,8 @@ // This Source Code Form is subject to the terms of the Mozilla Public

// rejects after 10 seconds
async function rejectAfter10s (error) {
await sleep(10 * 1000)
throw error
}
async function runOnPage (page, asyncFunc) {

@@ -31,2 +37,8 @@ // this html file loads ./bundle.js which is ./only-one-tab.js browserified

// returns tab object
// {
// page: (puppeteer page for this tab),
// isActing: (has the onlyOneTab callback been run yet?)
// actingPromise: (promise for when the onlyOneTab callback runs)
// }
async function newTab (browser) {

@@ -40,6 +52,13 @@ const page = await browser.newPage()

tab.promise = runOnPage(page, async () => {
tab.actingPromise = runOnPage(page, async () => {
const onlyOneTab = require('only-one-tab')
await new Promise((resolve) => onlyOneTab(resolve))
await new Promise((resolve) => {
onlyOneTab(() => {
// console logs for debugging with devtools
console.log('this tab started acting at ', new Date())
resolve()
})
})
})

@@ -55,26 +74,25 @@ // .then is required because the above async function runs on the page not here

test('open 20 tabs, then close each actor tab in sequence', async () => {
test('a closing tab should successfully signal a new actor', async () => {
const browser = await puppeteer.launch()
// repeat for consistency
for (let i = 0; i < 10; i++) {
const tabs = []
const tabs = []
for (let i = 0; i < 20; i++) {
const tab = await newTab(browser)
tabs.push(tab)
}
// start with one tab
tabs.push(await newTab(browser))
while (tabs.length > 0) {
// wait for a tab to become actor
await Promise.race(tabs.map((tab) => tab.promise))
// repeat to ensure consistency
for (let i = 0; i < 100; i++) {
// add one tab
tabs.push(await newTab(browser))
const actingTabs = tabs.filter((tab) => tab.isActing === true)
assert.equal(actingTabs.length, 1, 'there should be exactly one actor')
// wait for a tab to become actor
await Promise.race([
rejectAfter10s(new Error('timed out waiting for new tab to become actor')),
...tabs.map((tab) => tab.actingPromise)
])
// close the acting tab and remove from tabs[]
const actingIndex = actingTabs.findIndex((tab) => tab.isActing === true)
await tabs[actingIndex].page.close({ runBeforeUnload: true })
tabs.splice(actingIndex, 1)
}
// close the acting tab and remove from tabs[]
const actingIndex = tabs.findIndex((tab) => tab.isActing === true)
await tabs[actingIndex].page.close({ runBeforeUnload: true })
tabs.splice(actingIndex, 1)
}

@@ -85,35 +103,59 @@

test('open 20 tabs, then close them randomly', async () => {
test('there should be only one actor after opening many tabs concurrently', async () => {
const browser = await puppeteer.launch()
// repeat for consistency
for (let i = 0; i < 10; i++) {
const tabs = []
const openingTabs = []
for (let i = 0; i < 200; i++) {
// open in parallel to allow them to compete
openingTabs.push(newTab(browser))
}
for (let i = 0; i < 20; i++) {
const tab = await newTab(browser)
tabs.push(tab)
}
const tabs = await Promise.all(openingTabs)
while (tabs.length > 0) {
// wait until any tab is an actor
await Promise.race(tabs.map((tab) => tab.promise))
await Promise.race([
rejectAfter10s(new Error('timed out waiting for new tab to become actor')),
...tabs.map((tab) => tab.actingPromise)
])
const actingTabs = tabs.filter((tab) => tab.isActing === true)
assert.equal(actingTabs.length, 1, 'there should be exactly one actor')
const actingTabs = tabs.filter((tab) => tab.isActing === true)
assert.equal(actingTabs.length, 1, 'there should be exactly one actor')
const randomTabIndex = Math.floor(Math.random() * tabs.length)
browser.close()
})
// close random tab
await tabs[randomTabIndex].page.close({ runBeforeUnload: true })
// this reliably tests recovery from getting stuck
test('close all but one non-acting tab at once', async () => {
const browser = await puppeteer.launch()
// remove closed tab from tabs[]
tabs.splice(randomTabIndex, 1)
}
// open 99 tabs
const tabPromises = []
for (let i = 0; i < 99; i++) {
tabPromises.push(newTab(browser))
}
const tabsToClose = await Promise.all(tabPromises)
// wait for an actor
await Promise.race([
rejectAfter10s(new Error('timed out waiting for initial tab to become actor')),
...tabsToClose.map((tab) => tab.actingPromise)
])
const nonActor = await newTab(browser)
// close all tabs but nonActor concurrently
for (const tab of tabsToClose) {
tab.page.close({ runBeforeUnload: true })
}
// wait for nonActor to act
await Promise.race([
rejectAfter10s(new Error('last tab should act')),
nonActor.actingPromise
])
browser.close()
})
test('should reset after last tab closed', async () => {
test('should work when site is reopened', async () => {
const browser = await puppeteer.launch()

@@ -124,15 +166,16 @@

// wait for action
await tab.promise
await tab.actingPromise
await tab.page.close({ runBeforeUnload: true })
// make sure the last tab fully closes
await sleep(100)
const tab2 = await newTab(browser)
async function timeout () {
await sleep(10 * 1000)
throw new Error('timed out')
}
await Promise.race([
rejectAfter10s(new Error('timed out waiting for new tab to act')),
tab2.actingPromise
])
await Promise.race([timeout(), tab2.promise])
browser.close()

@@ -147,3 +190,3 @@ })

// wait until crashTab is the actor
await crashTab.promise
await crashTab.actingPromise

@@ -161,3 +204,3 @@ crashTab.page.on('error', () => {}) // this prevents unhandled rejection from crash

for (let i = 0; i < 100; i++) {
// create all tabs in parallel to allow them to compete for actor
// open many tabs concurrently to ensure that the reset doesn't cause multiple actors
tabPromises.push(newTab(browser))

@@ -168,5 +211,11 @@ }

// wait 10 seconds to ensure everything is settled
await sleep(10 * 1000)
// wait for an actor
await Promise.race([
rejectAfter10s(new Error('timed out waiting for new tab to act')),
...tabs.map((tab) => tab.actingPromise)
])
// wait for any other tabs to start acting
await sleep(2 * 1000)
const actors = tabs.filter((tab) => tab.isActing === true)

@@ -177,1 +226,62 @@ assert.equal(actors.length, 1, 'there should be exactly one actor')

})
test('open a lot of tabs, then close each actor tab in sequence', async () => {
const browser = await puppeteer.launch()
const tabPromises = []
for (let i = 0; i < 100; i++) {
tabPromises.push(newTab(browser))
}
const tabs = await Promise.all(tabPromises)
while (tabs.length > 0) {
// wait for a tab to become actor
await Promise.race([
rejectAfter10s(new Error('timed out waiting for new tab to become actor')),
...tabs.map((tab) => tab.actingPromise)
])
const actingTabs = tabs.filter((tab) => tab.isActing === true)
assert.equal(actingTabs.length, 1, 'there should be exactly one actor')
// close the acting tab and remove from tabs[]
const actingIndex = actingTabs.findIndex((tab) => tab.isActing === true)
await tabs[actingIndex].page.close({ runBeforeUnload: true })
tabs.splice(actingIndex, 1)
}
browser.close()
})
test('open a lot of tabs, then close them randomly', async () => {
const browser = await puppeteer.launch()
const tabPromises = []
for (let i = 0; i < 100; i++) {
tabPromises.push(newTab(browser))
}
const tabs = await Promise.all(tabPromises)
while (tabs.length > 0) {
// wait until any tab is an actor
await Promise.race([
rejectAfter10s(new Error('timed out waiting for new tab to become actor')),
...tabs.map((tab) => tab.actingPromise)
])
const actingTabs = tabs.filter((tab) => tab.isActing === true)
assert.equal(actingTabs.length, 1, 'there should be exactly one actor')
const randomTabIndex = Math.floor(Math.random() * tabs.length)
// close random tab
await tabs[randomTabIndex].page.close({ runBeforeUnload: true })
// remove closed tab from tabs[]
tabs.splice(randomTabIndex, 1)
}
browser.close()
})
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc