Explaining why the singleton pattern retains the previous value:
Why singleton pattern using previous value
The singleton pattern structure:
// ONE instance created when module loads
const playwrightManager = new PlaywrightBrowserManager();
// This instance has properties:
// - this.browser = null (initially)
// - this.context = null (initially)
The problem flow:
Scenario 1: First request
// Request A comes in
playwrightManager.launchBrowser({ headless: false })
→ this.browser = BrowserInstance1 (headless: false) ✅
→ Browser window opens
Scenario 2: Second request (the problem)
// Request B comes in 2 seconds later
playwrightManager.launchBrowser({ headless: true })
→ Checks: Does this.browser exist? YES (BrowserInstance1)
→ Checks: Is it connected? YES (still running)
→ Checks: Does headless match? NO (false vs true)
→ BUT: BrowserInstance1 was ALREADY launched with headless=false
→ BrowserInstance1 CANNOT change its headless setting after launch!
→ Returns the existing BrowserInstance1 (still headless: false) ❌
Why you can't change headless after launch:
// When you launch a browser:
const browser = await chromium.launch({ headless: false });
// The browser is NOW running with headless=false
// You CANNOT do: browser.setHeadless(true) ❌ (doesn't exist)
// The headless setting is baked into the browser process
// Once launched, it's fixed until you close and relaunch
Visual representation:
┌─────────────────────────────────────────────┐
│ Singleton Instance (ONE for entire app) │
│ ┌───────────────────────────────────────┐ │
│ │ this.browser = BrowserInstance1 │ │ ← Shared by ALL requests
│ │ - headless: false (fixed forever) │ │
│ │ - launched at: 10:00:00 │ │
│ │ - still connected: YES │ │
│ └───────────────────────────────────────┘ │
│ │
│ Request A (10:00:00): │
│ launchBrowser({headless: false}) │
│ → Creates BrowserInstance1 ✅ │
│ │
│ Request B (10:00:02): │
│ launchBrowser({headless: true}) │
│ → Sees BrowserInstance1 exists │
│ → Tries to reuse it │
│ → But it's still headless: false ❌ │
└─────────────────────────────────────────────┘
The code that caused the issue:
// In the old singleton code:
if (this.browser && this.browser.isConnected()) {
// Browser exists and is running
if (currentHeadless === headless) {
// Settings match - reuse browser ✅
return { browser: this.browser, context: this.context };
} else {
// Settings DON'T match, but browser is still connected
// What happens? It might still return the old browser!
// OR it creates a new one but doesn't close the old one properly
}
}
Why separate instances fix it:
// Request A
const managerA = playwrightManager(); // NEW instance
managerA.launchBrowser({ headless: false })
→ managerA.browser = BrowserInstance1 (headless: false) ✅
// Request B (simultaneously)
const managerB = playwrightManager(); // DIFFERENT instance
managerB.launchBrowser({ headless: true })
→ managerB.browser = BrowserInstance2 (headless: true) ✅
→ No conflict! Each has its own browser
Key points:
- Browser headless setting is immutable after launch — you can't change it.
- Singleton shares one browser instance — all requests use the same
this.browser. - Reuse logic — if a browser exists and is connected, it may be reused even with different settings.
- Timing — if Request A's browser is still running when Request B starts, Request B might get the old browser.
The fix:
With separate instances:
- Each request gets its own
PlaywrightBrowserManagerinstance - Each instance has its own
this.browserproperty - No sharing = no conflicts
- Each request controls its own browser lifecycle
That's why separate instances solve the problem — each request gets a fresh browser manager with its own browser instance, so settings aren't shared or reused incorrectly.
Top comments (0)