← Public packages
@kody/browser-run
Call Cloudflare Browser Run Quick Actions and reuse shared sessions with createBrowserContext.
src/open-isolated.ts
140 lines · 4.5 KB · TypeScriptimport { boolean, number, object, optional, parse, string } from 'remix/data-schema'
import {
ISOLATION_INSTRUCTIONS,
authorizationHeader,
browserWsEndpoint,
browserWsEndpointAlias,
pickAuth,
requireAccountId,
} from './client.ts'
import acquireSession from './sessions/acquire.ts'
import listSessions from './sessions/list.ts'
const openInput = object(
{
accountId: string(),
apiTokenSecret: optional(string()),
keepAliveMs: optional(number()),
/** When true, only list + plan reuse; do not POST acquire. */
dryRun: optional(boolean()),
/** Force launch even if sessions exist. */
forceLaunch: optional(boolean()),
},
{ unknownKeys: 'error' },
)
function shuffleStart<T>(items: T[]): T[] {
if (items.length <= 1) return items.slice()
const start = Math.floor(Math.random() * items.length)
return [...items.slice(start), ...items.slice(0, start)]
}
function sessionIdOf(session: unknown): string | null {
if (!session || typeof session !== 'object') return null
const s = session as Record<string, unknown>
const id = s.sessionId ?? s.session_id ?? s.id
return id != null ? String(id).trim() || null : null
}
function wsOf(session: unknown): string | null {
if (!session || typeof session !== 'object') return null
const s = session as Record<string, unknown>
const url = s.webSocketDebuggerUrl ?? s.browserWSEndpoint
return url != null ? String(url).trim() || null : null
}
/**
* DEFAULT shared-session path (pit of success): list active sessions, try to
* reuse one (random start order like Cloudflare docs), otherwise acquire a
* new session. Returns connect info plus hard instructions to
* `createBrowserContext()`, then `context.close()` + `browser.disconnect()` —
* never `browser.close()` while sharing.
*
* Puppeteer/Playwright run outside Kody (Worker or Node). This export only
* prepares session + CDP connection metadata via the REST API.
*
* @param raw.accountId - Cloudflare account id
* @param raw.keepAliveMs - Keep-alive when launching
* @param raw.forceLaunch - Skip reuse and acquire fresh
* @param raw.dryRun - List + plan only; never acquire
* @param raw.apiTokenSecret - Optional alternate secret name
* @returns `{ sessionId, browserWSEndpoint, launched, isolation, instructions }`
*
* @example
* import openIsolated from 'kody:@kody/browser-run/open-isolated'
* const session = await openIsolated({ accountId: 'YOUR_ACCOUNT_ID' })
* // Connect externally, then createBrowserContext → work → context.close → disconnect
*/
export default async function openIsolated(raw: unknown = {}) {
const input = parse(openInput, raw ?? {})
const auth = pickAuth(input)
const accountId = requireAccountId(auth)
const listed = await listSessions({
...auth,
accountId,
})
const sessions = Array.isArray(listed.sessions) ? listed.sessions : []
const ordered = shuffleStart(sessions)
if (input.dryRun === true) {
return {
dryRun: true as const,
accountId,
activeSessionCount: sessions.length,
candidateSessionIds: ordered.map(sessionIdOf).filter(Boolean),
wouldReuse: !input.forceLaunch && ordered.length > 0,
wouldLaunch: input.forceLaunch === true || ordered.length === 0,
isolation: 'createBrowserContext' as const,
disconnectNotClose: true as const,
instructions: ISOLATION_INSTRUCTIONS,
}
}
if (!input.forceLaunch) {
for (const session of ordered) {
const sessionId = sessionIdOf(session)
if (!sessionId) continue
const browserWSEndpoint =
wsOf(session) || browserWsEndpoint(accountId, sessionId, input.keepAliveMs)
return {
ok: true,
accountId,
sessionId,
browserWSEndpoint,
browserWSEndpointAlias: browserWsEndpointAlias(
accountId,
sessionId,
input.keepAliveMs,
),
authorizationHeaderTemplate: authorizationHeader(auth),
launched: false,
isolation: 'createBrowserContext' as const,
disconnectNotClose: true as const,
keepAliveMs: input.keepAliveMs ?? null,
reusedFromList: true,
candidatesTried: 1,
activeSessionCount: sessions.length,
instructions: ISOLATION_INSTRUCTIONS,
note:
'Listed session may have expired or be at capacity — if puppeteer.connect fails, call open-isolated again with forceLaunch: true or retry.',
}
}
}
const acquired = await acquireSession({
...auth,
accountId,
keepAliveMs: input.keepAliveMs,
})
return {
...acquired,
launched: true,
reusedFromList: false,
activeSessionCount: sessions.length,
isolation: 'createBrowserContext' as const,
disconnectNotClose: true as const,
instructions: ISOLATION_INSTRUCTIONS,
}
}