Skip to content

Commit 9aab517

Browse files
committed
vibe code
1 parent 83cafef commit 9aab517

5 files changed

Lines changed: 676 additions & 0 deletions

File tree

examples/browser-routing-smoke.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/**
2+
* Smoke test for the demo metro-direct routing middleware.
3+
*
4+
* Runs the local source (not the published build) thanks to the tsconfig path
5+
* alias `@onkernel/sdk` -> `./src/index.ts`, wired up by `yarn tsn`.
6+
*
7+
* Usage (from the repo root):
8+
*
9+
* cd /Users/sayan/kernel/kernel-node-sdk
10+
* yarn install # if you haven't already
11+
* KERNEL_API_KEY=sk-... yarn tsn examples/browser-routing-smoke.ts
12+
*
13+
* Optional env vars:
14+
* KERNEL_BASE_URL - override the API base (defaults to production)
15+
* SKIP_COMPARE - if set, skip the public-API timing comparison
16+
*
17+
* What this verifies:
18+
* 1. browsers.create() returns a Browser whose base_url + cdp_ws_url
19+
* let us derive a metro-direct route.
20+
* 2. The routing cache gets populated automatically (no manual prewarm).
21+
* 3. A subresource call (computer.clickMouse) actually succeeds when
22+
* routed to <base_url>/computer/click_mouse?jwt=...
23+
* 4. (Optional) timing comparison vs. the public-API path.
24+
*
25+
* If anything fails, the browser is still cleaned up.
26+
*/
27+
28+
import Kernel from '@onkernel/sdk';
29+
30+
const SUBSEP = '─'.repeat(60);
31+
32+
function log(...args: unknown[]) {
33+
console.log(...args);
34+
}
35+
function header(s: string) {
36+
console.log('\n' + SUBSEP + '\n' + s + '\n' + SUBSEP);
37+
}
38+
39+
async function timeIt<T>(label: string, fn: () => Promise<T>): Promise<{ value: T; ms: number }> {
40+
const t0 = Date.now();
41+
const value = await fn();
42+
const ms = Date.now() - t0;
43+
log(` ${label}: ${ms} ms`);
44+
return { value, ms };
45+
}
46+
47+
async function main() {
48+
if (!process.env['KERNEL_API_KEY']) {
49+
console.error('Set KERNEL_API_KEY before running this script.');
50+
process.exit(2);
51+
}
52+
53+
// Routed client (opt-in to metro-direct).
54+
const routed = new Kernel({
55+
browserRouting: { enabled: true },
56+
logLevel: 'debug',
57+
});
58+
59+
// Plain client for the side-by-side comparison; same API key, no routing.
60+
const plain = new Kernel({ logLevel: 'warn' });
61+
62+
header('1) Create a browser and inspect routing-relevant fields');
63+
const browser = await routed.browsers.create({});
64+
log(' session_id:', browser.session_id);
65+
log(' base_url: ', browser.base_url ?? '<empty>');
66+
log(' cdp_ws_url:', browser.cdp_ws_url);
67+
68+
let exitCode = 0;
69+
try {
70+
header('2) Verify cache was populated by the create response');
71+
const cached = routed.browserRouteCache?.get(browser.session_id);
72+
log(' cache entry:', cached);
73+
if (!cached) {
74+
console.error(
75+
' FAIL: cache was not populated. Either base_url is empty in this env,',
76+
'\n or cdp_ws_url has no `?jwt=` query param.',
77+
);
78+
exitCode = 1;
79+
return;
80+
}
81+
if (!browser.base_url) {
82+
console.error(' FAIL: base_url was empty even though we cached something — bug in extractor.');
83+
exitCode = 1;
84+
return;
85+
}
86+
87+
header('3) Call computer.clickMouse via metro-direct (watch debug log)');
88+
const routedCall = await timeIt('metro-direct call', () =>
89+
routed.browsers.computer.clickMouse(browser.session_id, { x: 10, y: 10 }),
90+
);
91+
void routedCall;
92+
93+
if (!process.env['SKIP_COMPARE']) {
94+
header('4) Same call via the public API for comparison');
95+
const plainCall = await timeIt('public-API call', () =>
96+
plain.browsers.computer.clickMouse(browser.session_id, { x: 20, y: 20 }),
97+
);
98+
void plainCall;
99+
100+
header('5) Repeat both, 3x each, to get a steady-state read');
101+
const routedSamples: number[] = [];
102+
const plainSamples: number[] = [];
103+
for (let i = 0; i < 3; i++) {
104+
const r = await timeIt(`metro-direct #${i + 1}`, () =>
105+
routed.browsers.computer.clickMouse(browser.session_id, { x: 30 + i, y: 30 + i }),
106+
);
107+
routedSamples.push(r.ms);
108+
const p = await timeIt(`public-API #${i + 1}`, () =>
109+
plain.browsers.computer.clickMouse(browser.session_id, { x: 40 + i, y: 40 + i }),
110+
);
111+
plainSamples.push(p.ms);
112+
}
113+
const avg = (xs: number[]) => Math.round(xs.reduce((a, b) => a + b, 0) / xs.length);
114+
header('6) Result');
115+
log(` metro-direct avg: ${avg(routedSamples)} ms (samples: ${routedSamples.join(', ')})`);
116+
log(` public-API avg: ${avg(plainSamples)} ms (samples: ${plainSamples.join(', ')})`);
117+
log(` delta: ${avg(plainSamples) - avg(routedSamples)} ms`);
118+
}
119+
120+
log('\nOK');
121+
} catch (err) {
122+
console.error('\nERROR during routed flow:', err);
123+
exitCode = 1;
124+
} finally {
125+
header('cleanup');
126+
try {
127+
await plain.browsers.deleteByID(browser.session_id);
128+
log(' deleted', browser.session_id);
129+
} catch (e) {
130+
console.error(' failed to delete browser:', e);
131+
}
132+
process.exit(exitCode);
133+
}
134+
}
135+
136+
void main();

src/client.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { AbstractPage, type OffsetPaginationParams, OffsetPaginationResponse } f
1919
import * as Uploads from './core/uploads';
2020
import * as API from './resources/index';
2121
import { APIPromise } from './core/api-promise';
22+
import { BrowserRouteCache, createRoutingFetch } from './lib/browser-routing';
2223
import { AppListParams, AppListResponse, AppListResponsesOffsetPagination, Apps } from './resources/apps';
2324
import {
2425
BrowserPool,
@@ -231,6 +232,17 @@ export interface ClientOptions {
231232
* Defaults to globalThis.console.
232233
*/
233234
logger?: Logger | undefined;
235+
236+
/**
237+
* Opt in to transparent metro-direct routing for browser subresource calls.
238+
* When enabled, calls like `browsers.process.exec(id, ...)` are routed
239+
* directly to the metro-api proxy when the SDK has seen a Browser response
240+
* for `id` in the current process. Falls back transparently to the public
241+
* API on cache miss or on 401/403/404 from metro.
242+
*
243+
* Demo flag — off by default to keep the default behavior unchanged.
244+
*/
245+
browserRouting?: { enabled?: boolean; cache?: BrowserRouteCache } | undefined;
234246
}
235247

236248
/**
@@ -247,6 +259,8 @@ export class Kernel {
247259
fetchOptions: MergedRequestInit | undefined;
248260

249261
private fetch: Fetch;
262+
/** Exposed for debugging/demo — inspect or prewarm the metro-direct route cache. */
263+
public browserRouteCache?: BrowserRouteCache;
250264
#encoder: Opts.RequestEncoder;
251265
protected idempotencyHeader?: string;
252266
private _options: ClientOptions;
@@ -313,6 +327,14 @@ export class Kernel {
313327
this.fetchOptions = options.fetchOptions;
314328
this.maxRetries = options.maxRetries ?? 2;
315329
this.fetch = options.fetch ?? Shims.getDefaultFetch();
330+
if (options.browserRouting?.enabled) {
331+
this.browserRouteCache = options.browserRouting.cache ?? new BrowserRouteCache();
332+
this.fetch = createRoutingFetch({
333+
apiBaseURL: this.baseURL,
334+
inner: this.fetch,
335+
cache: this.browserRouteCache,
336+
});
337+
}
316338
this.#encoder = Opts.FallbackEncoder;
317339

318340
this._options = options;

0 commit comments

Comments
 (0)