Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ lib/v3/dom/build/
packages/core/dist/
packages/core/lib/dom/build/
packages/core/lib/v3/dom/build/
packages/core/gen/
packages/evals/dist/
packages/docs/
*.min.js
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default [
"**/node_modules/**",
"packages/core/lib/dom/build/**",
"packages/core/lib/v3/dom/build/**",
"packages/core/gen/**",
"**/*.config.js",
"**/*.config.mjs",
],
Expand Down
6 changes: 6 additions & 0 deletions packages/core/buf.gen.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: v2
plugins:
- local: protoc-gen-es
out: gen
include_imports: true
opt: target=ts
6 changes: 6 additions & 0 deletions packages/core/buf.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Generated by buf. DO NOT EDIT.
version: v2
deps:
- name: buf.build/bufbuild/protovalidate
commit: 52f32327d4b045a79293a6ad4e7e1236
digest: b5:cbabc98d4b7b7b0447c9b15f68eeb8a7a44ef8516cb386ac5f66e7fd4062cd6723ed3f452ad8c384b851f79e33d26e7f8a94e2b807282b3def1cd966c7eace97
11 changes: 11 additions & 0 deletions packages/core/buf.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: v2
modules:
- path: proto
deps:
- buf.build/bufbuild/protovalidate
lint:
use:
- STANDARD
breaking:
use:
- FILE
4,963 changes: 4,963 additions & 0 deletions packages/core/gen/buf/validate/validate_pb.ts

Large diffs are not rendered by default.

88 changes: 88 additions & 0 deletions packages/core/gen/stagehand/v1/ping_pb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// @generated by protoc-gen-es v2.10.1 with parameter "target=ts"
// @generated from file stagehand/v1/ping.proto (package stagehand.v1, syntax proto3)
/* eslint-disable */

import type {
GenFile,
GenMessage,
GenService,
} from "@bufbuild/protobuf/codegenv2";
import {
fileDesc,
messageDesc,
serviceDesc,
} from "@bufbuild/protobuf/codegenv2";
import type { Timestamp } from "@bufbuild/protobuf/wkt";
import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt";
import type { Message } from "@bufbuild/protobuf";

/**
* Describes the file stagehand/v1/ping.proto.
*/
export const file_stagehand_v1_ping: GenFile =
/*@__PURE__*/
fileDesc(
"ChdzdGFnZWhhbmQvdjEvcGluZy5wcm90bxIMc3RhZ2VoYW5kLnYxIkMKC1BpbmdSZXF1ZXN0EjQKEGNsaWVudF9zZW5kX3RpbWUYASABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wInoKDFBpbmdSZXNwb25zZRI0ChBjbGllbnRfc2VuZF90aW1lGAEgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBI0ChBzZXJ2ZXJfc2VuZF90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcDJVChRTdGFnZWhhbmRQaW5nU2VydmljZRI9CgRQaW5nEhkuc3RhZ2VoYW5kLnYxLlBpbmdSZXF1ZXN0Ghouc3RhZ2VoYW5kLnYxLlBpbmdSZXNwb25zZWIGcHJvdG8z",
[file_google_protobuf_timestamp],
);

/**
* @generated from message stagehand.v1.PingRequest
*/
export type PingRequest = Message<"stagehand.v1.PingRequest"> & {
/**
* Timestamp representing when the client emitted the ping.
*
* @generated from field: google.protobuf.Timestamp client_send_time = 1;
*/
clientSendTime?: Timestamp;
};

/**
* Describes the message stagehand.v1.PingRequest.
* Use `create(PingRequestSchema)` to create a new message.
*/
export const PingRequestSchema: GenMessage<PingRequest> =
/*@__PURE__*/
messageDesc(file_stagehand_v1_ping, 0);

/**
* @generated from message stagehand.v1.PingResponse
*/
export type PingResponse = Message<"stagehand.v1.PingResponse"> & {
/**
* Echo of the client's send time so latency can be derived from RTT.
*
* @generated from field: google.protobuf.Timestamp client_send_time = 1;
*/
clientSendTime?: Timestamp;

/**
* Timestamp representing when the server crafted the response.
*
* @generated from field: google.protobuf.Timestamp server_send_time = 2;
*/
serverSendTime?: Timestamp;
};

/**
* Describes the message stagehand.v1.PingResponse.
* Use `create(PingResponseSchema)` to create a new message.
*/
export const PingResponseSchema: GenMessage<PingResponse> =
/*@__PURE__*/
messageDesc(file_stagehand_v1_ping, 1);

/**
* @generated from service stagehand.v1.StagehandPingService
*/
export const StagehandPingService: GenService<{
/**
* @generated from rpc stagehand.v1.StagehandPingService.Ping
*/
ping: {
methodKind: "unary";
input: typeof PingRequestSchema;
output: typeof PingResponseSchema;
};
}> = /*@__PURE__*/ serviceDesc(file_stagehand_v1_ping, 0);
15 changes: 14 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,18 @@
"typecheck": "tsc --noEmit",
"prepare": "pnpm run build",
"build": "pnpm run gen-version && pnpm run build-dom-scripts && pnpm run build-js && pnpm run typecheck",
"dev": "tsx server/server.ts",
"example": "node --import tsx -e \"const args=process.argv.slice(1).filter(a=>a!=='--'); const [p]=args; const n=(p||'example').replace(/^\\.\\//,'').replace(/\\.ts$/i,''); import(new URL(require('node:path').resolve('examples', n + '.ts'), 'file:'));\" --",
"test": "playwright test --config=lib/v3/tests/v3.playwright.config.ts",
"e2e": "playwright test --config=lib/v3/tests/v3.local.playwright.config.ts",
"e2e:local": "playwright test --config=lib/v3/tests/v3.local.playwright.config.ts",
"e2e:bb": "playwright test --config=lib/v3/tests/v3.bb.playwright.config.ts",
"lint": "cd ../.. && prettier --check packages/core && cd packages/core && eslint .",
"format": "prettier --write .",
"test:vitest": "pnpm run build-js && pnpm run typecheck && vitest run --config vitest.config.ts"
"test:vitest": "pnpm run build-js && pnpm run typecheck && vitest run --config vitest.config.ts",
"rpc:deps": "pnpm exec buf dep update",
"rpc:lint": "pnpm exec buf lint",
"rpc:generate": "pnpm exec buf generate"
},
"files": [
"dist/index.js",
Expand All @@ -46,11 +50,18 @@
"@ai-sdk/provider": "^2.0.0",
"@anthropic-ai/sdk": "0.39.0",
"@browserbasehq/sdk": "^2.4.0",
"@bufbuild/protobuf": "^2.10.1",
"@connectrpc/connect": "^2.1.1",
"@connectrpc/connect-fastify": "^2.1.1",
"@connectrpc/connect-node": "^2.1.1",
"@connectrpc/validate": "^0.2.0",
"@fastify/cors": "^10.0.1",
"@google/genai": "^1.22.0",
"@langchain/openai": "^0.4.4",
"@modelcontextprotocol/sdk": "^1.17.2",
"ai": "^5.0.0",
"devtools-protocol": "^0.0.1464554",
"fastify": "^5.2.4",
"fetch-cookie": "^3.1.0",
"openai": "^4.87.1",
"pino": "^9.6.0",
Expand Down Expand Up @@ -80,6 +91,8 @@
"puppeteer-core": "^22.8.0"
},
"devDependencies": {
"@bufbuild/buf": "^1.61.0",
"@bufbuild/protoc-gen-es": "^2.10.1",
"@playwright/test": "^1.42.1",
"eslint": "^9.16.0",
"prettier": "^3.2.5",
Expand Down
22 changes: 22 additions & 0 deletions packages/core/proto/stagehand/v1/ping.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
syntax = "proto3";

package stagehand.v1;

import "google/protobuf/timestamp.proto";

message PingRequest {
// Timestamp representing when the client emitted the ping.
google.protobuf.Timestamp client_send_time = 1;
}

message PingResponse {
// Echo of the client's send time so latency can be derived from RTT.
google.protobuf.Timestamp client_send_time = 1;

// Timestamp representing when the server crafted the response.
google.protobuf.Timestamp server_send_time = 2;
}

service StagehandPingService {
rpc Ping(PingRequest) returns (PingResponse);
}
28 changes: 28 additions & 0 deletions packages/core/server/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { type ConnectRouter } from "@connectrpc/connect";
import { timestampNow } from "@bufbuild/protobuf/wkt";
import { createValidateInterceptor } from "@connectrpc/validate";
import { fastify } from "fastify";
import { fastifyConnectPlugin } from "@connectrpc/connect-fastify";
import { StagehandPingService } from "../gen/stagehand/v1/ping_pb";

export const routes = (router: ConnectRouter) =>
router.service(StagehandPingService, {
async ping(req) {
return {
clientSendTime: req.clientSendTime,
serverSendTime: timestampNow(),
};
},
});

async function main() {
const server = fastify();
await server.register(fastifyConnectPlugin, {
interceptors: [createValidateInterceptor()],
routes,
});
await server.listen({ host: "localhost", port: 8080 });
console.log("server is listening at", server.addresses());
}

void main();
96 changes: 96 additions & 0 deletions packages/core/tests/server/ping.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { beforeAll, afterAll, describe, expect, it } from "vitest";
import { fastify } from "fastify";
import { createValidateInterceptor } from "@connectrpc/validate";
import { fastifyConnectPlugin } from "@connectrpc/connect-fastify";
import { Code, createClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-node";
import { timestampFromMs, timestampMs } from "@bufbuild/protobuf/wkt";
import { routes } from "../../server/server";
import { StagehandPingService } from "../../gen/stagehand/v1/ping_pb";

const TEST_HOST = "127.0.0.1";
const TEST_PORT = 0; // use ephemeral port

describe("Stagehand Ping RPC", () => {
const server = fastify();
let baseUrl: string;

beforeAll(async () => {
await server.register(fastifyConnectPlugin, {
interceptors: [createValidateInterceptor()],
routes,
});
baseUrl = await server.listen({ host: TEST_HOST, port: TEST_PORT });
});

afterAll(async () => {
await server.close();
});

it("responds with server timestamp echoing client timestamp", async () => {
const client = createClient(
StagehandPingService,
createConnectTransport({ baseUrl, httpVersion: "1.1" }),
);
const clientSendTime = Date.now();
const response = await client.ping({
clientSendTime: timestampFromMs(clientSendTime),
});
expect(timestampMs(response.clientSendTime)).toBe(clientSendTime);
expect(timestampMs(response.serverSendTime)).toBeGreaterThanOrEqual(
clientSendTime,
);
});

it("rejects invalid timestamp structure", async () => {
const client = createClient(
StagehandPingService,
createConnectTransport({ baseUrl, httpVersion: "1.1" }),
);
await expect(
client.ping({
clientSendTime: {
seconds: "not a bigint" as never, // Wrong type
nanos: "not a number" as never, // Wrong type
} as never,
}),
).rejects.toMatchObject({
code: Code.Internal,
});
});

it("calculates RTT, latency, and clock offset correctly", async () => {
const client = createClient(
StagehandPingService,
createConnectTransport({ baseUrl, httpVersion: "1.1" }),
);

const t0 = Date.now();
const pingResponse = await client.ping({
clientSendTime: timestampFromMs(t0),
});

const t3 = Date.now();
const rtt = t3 - t0;
const latency = rtt / 2;
const clientSendTimeMs = timestampMs(pingResponse.clientSendTime);
const serverSendTimeMs = timestampMs(pingResponse.serverSendTime);
const offset = serverSendTimeMs - (t0 + latency);

// RTT should be positive and reasonable (less than 1 second for local test)
expect(rtt).toBeGreaterThan(0);
expect(rtt).toBeLessThan(1000);

// Latency should be half of RTT
expect(latency).toBe(rtt / 2);

// Client send time should match what we sent
expect(clientSendTimeMs).toBe(t0);

// Server send time should be after client send time
expect(serverSendTimeMs).toBeGreaterThanOrEqual(t0);

// Clock offset should be reasonable (within a few seconds for local test)
expect(Math.abs(offset)).toBeLessThan(5000);
});
});
2 changes: 1 addition & 1 deletion packages/core/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@
"declaration": true,
"emitDeclarationOnly": true
},
"include": ["lib/**/*", "examples/**/*", "tests/**/*"],
"include": ["lib/**/*", "examples/**/*", "tests/**/*", "server/**/*"],
"exclude": ["node_modules", "dist"]
}
Loading