@vrealtime/server 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ import type { AgentDefinition } from '@vrealtime/core';
2
+ import { Hub, type AttachOptions } from '../index.js';
3
+ export interface ServeOptions extends AttachOptions {
4
+ /** Default 8790; 0 picks a free port. */
5
+ port?: number;
6
+ /** Default `localhost`. */
7
+ host?: string;
8
+ /** Serve static files from this directory; its `index.html` replaces the debug page. */
9
+ publicDir?: string | URL;
10
+ /** Called once listening; logs the URL by default. */
11
+ onListening?: (url: string) => void;
12
+ }
13
+ export interface RunningServer {
14
+ url: string;
15
+ port: number;
16
+ hub: Hub;
17
+ close(): Promise<void>;
18
+ }
19
+ /**
20
+ * Serves `agent` on Node: WebSocket protocol v1 on every path, the microphone
21
+ * debug page (or `publicDir`) over HTTP. Needs the optional peer dependency `ws`.
22
+ */
23
+ export declare function serve(agent: AgentDefinition, options?: ServeOptions): Promise<RunningServer>;
@@ -0,0 +1,120 @@
1
+ /// <reference types="node" />
2
+ import { readFile } from 'node:fs/promises';
3
+ import { createServer } from 'node:http';
4
+ import { extname, resolve, sep } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { Hub } from '../index.js';
7
+ import { DEBUG_PAGE } from './debug-page.js';
8
+ const CONTENT_TYPES = {
9
+ '.html': 'text/html; charset=utf-8',
10
+ '.js': 'text/javascript; charset=utf-8',
11
+ '.mjs': 'text/javascript; charset=utf-8',
12
+ '.css': 'text/css; charset=utf-8',
13
+ '.json': 'application/json',
14
+ '.map': 'application/json',
15
+ '.svg': 'image/svg+xml',
16
+ '.png': 'image/png',
17
+ '.ico': 'image/x-icon',
18
+ '.wasm': 'application/wasm',
19
+ };
20
+ function toHeaders(req) {
21
+ const headers = new Headers();
22
+ for (const [name, value] of Object.entries(req.headers)) {
23
+ if (Array.isArray(value))
24
+ for (const v of value)
25
+ headers.append(name, v);
26
+ else if (value !== undefined)
27
+ headers.set(name, value);
28
+ }
29
+ return headers;
30
+ }
31
+ const utf8 = new TextDecoder();
32
+ function toBytes(data) {
33
+ if (Array.isArray(data))
34
+ return Buffer.concat(data);
35
+ return data instanceof ArrayBuffer ? new Uint8Array(data) : data;
36
+ }
37
+ /**
38
+ * Serves `agent` on Node: WebSocket protocol v1 on every path, the microphone
39
+ * debug page (or `publicDir`) over HTTP. Needs the optional peer dependency `ws`.
40
+ */
41
+ export async function serve(agent, options = {}) {
42
+ let ws;
43
+ try {
44
+ ws = await import('ws');
45
+ }
46
+ catch {
47
+ throw new Error('@vrealtime/server/node: serve() needs the optional peer dependency "ws" (npm i ws)');
48
+ }
49
+ const hub = new Hub(agent, options);
50
+ const publicDir = options.publicDir === undefined ? undefined : resolve(options.publicDir instanceof URL ? fileURLToPath(options.publicDir) : options.publicDir);
51
+ const wss = new ws.WebSocketServer({ noServer: true });
52
+ async function handle(req, res) {
53
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
54
+ res.writeHead(405).end();
55
+ return;
56
+ }
57
+ const path = decodeURIComponent(new URL(req.url ?? '/', 'http://localhost').pathname);
58
+ if (publicDir !== undefined) {
59
+ const file = resolve(publicDir, `.${path.endsWith('/') ? `${path}index.html` : path}`);
60
+ if (file !== publicDir && !file.startsWith(publicDir + sep)) {
61
+ res.writeHead(403).end();
62
+ return;
63
+ }
64
+ try {
65
+ const body = await readFile(file);
66
+ res.writeHead(200, { 'content-type': CONTENT_TYPES[extname(file)] ?? 'application/octet-stream' }).end(req.method === 'HEAD' ? undefined : body);
67
+ }
68
+ catch {
69
+ res.writeHead(404).end('Not found');
70
+ }
71
+ return;
72
+ }
73
+ if (path !== '/') {
74
+ res.writeHead(404).end('Not found');
75
+ return;
76
+ }
77
+ res.writeHead(200, { 'content-type': CONTENT_TYPES['.html'] }).end(req.method === 'HEAD' ? undefined : DEBUG_PAGE);
78
+ }
79
+ const server = createServer((req, res) => {
80
+ handle(req, res).catch(() => res.writeHead(500).end());
81
+ });
82
+ server.on('upgrade', (req, socket, head) => {
83
+ wss.handleUpgrade(req, socket, head, (client) => {
84
+ const connection = hub.attach({
85
+ send: (data) => client.send(data),
86
+ close: (code, reason) => client.close(code, reason),
87
+ get bufferedAmount() {
88
+ return client.bufferedAmount;
89
+ },
90
+ }, { headers: toHeaders(req) });
91
+ client.on('message', (data, isBinary) => connection.receive(isBinary ? toBytes(data) : utf8.decode(toBytes(data))));
92
+ client.on('close', () => connection.closed());
93
+ client.on('error', () => connection.closed());
94
+ });
95
+ });
96
+ const host = options.host ?? 'localhost';
97
+ await new Promise((resolveListen, reject) => {
98
+ server.once('error', reject);
99
+ server.listen(options.port ?? 8790, host, () => {
100
+ server.off('error', reject);
101
+ resolveListen();
102
+ });
103
+ });
104
+ const address = server.address();
105
+ const port = typeof address === 'object' && address ? address.port : (options.port ?? 8790);
106
+ const url = `http://${host}:${port}`;
107
+ (options.onListening ?? ((u) => console.log(`vRealTime listening on ${u}`)))(url);
108
+ return {
109
+ url,
110
+ port,
111
+ hub,
112
+ async close() {
113
+ for (const client of wss.clients)
114
+ client.terminate();
115
+ await hub.close();
116
+ wss.close();
117
+ await new Promise((done) => server.close(() => done()));
118
+ },
119
+ };
120
+ }
@@ -0,0 +1,8 @@
1
+ import type { ToolStreamEvent } from '@vrealtime/core';
2
+ import type { ServerMessage, Unsequenced } from '@vrealtime/protocol';
3
+ type Outgoing = Unsequenced<ServerMessage>;
4
+ /** With `tool-stream-v1`: the step as a `tool.stream` message. A server tool's output never leaves the server. */
5
+ export declare function toolStreamMessage(event: ToolStreamEvent): Outgoing;
6
+ /** Without the capability: argument fragments and progress as the `tool.call.delta` preview of protocol 1.0; nothing else is sent. */
7
+ export declare function legacyPreview(event: ToolStreamEvent): Outgoing | undefined;
8
+ export {};
@@ -0,0 +1,29 @@
1
+ /** With `tool-stream-v1`: the step as a `tool.stream` message. A server tool's output never leaves the server. */
2
+ export function toolStreamMessage(event) {
3
+ const call = { type: 'tool.stream', callId: event.callId, epoch: event.epoch, turnId: event.turnId, name: event.name };
4
+ switch (event.phase) {
5
+ case 'start':
6
+ case 'cancelled':
7
+ return { ...call, phase: event.phase };
8
+ case 'arguments-delta':
9
+ return { ...call, phase: event.phase, delta: event.delta };
10
+ // Required members on the wire: a missing value is sent as null.
11
+ case 'arguments-complete':
12
+ return { ...call, phase: event.phase, arguments: event.arguments ?? null };
13
+ case 'progress':
14
+ return { ...call, phase: event.phase, progress: event.progress ?? null };
15
+ case 'settled':
16
+ return event.result.ok ? { ...call, phase: 'settled', ok: true } : { ...call, phase: 'settled', ok: false, code: event.result.code };
17
+ case 'failed':
18
+ return { ...call, phase: event.phase, message: event.message };
19
+ }
20
+ }
21
+ /** Without the capability: argument fragments and progress as the `tool.call.delta` preview of protocol 1.0; nothing else is sent. */
22
+ export function legacyPreview(event) {
23
+ const call = { type: 'tool.call.delta', callId: event.callId, epoch: event.epoch, name: event.name };
24
+ if (event.phase === 'arguments-delta')
25
+ return { ...call, preview: { argumentsDelta: event.delta } };
26
+ if (event.phase === 'progress')
27
+ return { ...call, preview: event.progress ?? null };
28
+ return undefined;
29
+ }
@@ -0,0 +1,15 @@
1
+ import type { AgentDefinition } from '@vrealtime/core';
2
+ import { Hub, type AttachOptions } from '../index.js';
3
+ /**
4
+ * Upgrades a request to a vRealTime session on Cloudflare Workers or inside a
5
+ * Durable Object's `fetch()`. Sessions live in memory (per isolate, or per object),
6
+ * so route reconnects to the same Durable Object to resume. Use the standard,
7
+ * non-hibernating WebSocket API.
8
+ *
9
+ * ```ts
10
+ * export default { fetch: (request) => upgrade(agent, request) };
11
+ * ```
12
+ */
13
+ export declare function upgrade(agent: AgentDefinition | Hub, request: Request, options?: AttachOptions): Response;
14
+ export type { AttachOptions, AuthRequest, Connection, Principal } from '../index.js';
15
+ export { Hub };
@@ -0,0 +1,37 @@
1
+ import { Hub } from '../index.js';
2
+ const hubs = new WeakMap();
3
+ function hubFor(agent, options) {
4
+ if (agent instanceof Hub)
5
+ return agent;
6
+ let hub = hubs.get(agent);
7
+ if (!hub) {
8
+ hub = new Hub(agent, options);
9
+ hubs.set(agent, hub);
10
+ }
11
+ return hub;
12
+ }
13
+ /**
14
+ * Upgrades a request to a vRealTime session on Cloudflare Workers or inside a
15
+ * Durable Object's `fetch()`. Sessions live in memory (per isolate, or per object),
16
+ * so route reconnects to the same Durable Object to resume. Use the standard,
17
+ * non-hibernating WebSocket API.
18
+ *
19
+ * ```ts
20
+ * export default { fetch: (request) => upgrade(agent, request) };
21
+ * ```
22
+ */
23
+ export function upgrade(agent, request, options) {
24
+ if (request.headers.get('Upgrade')?.toLowerCase() !== 'websocket') {
25
+ return new Response('Expected a WebSocket upgrade', { status: 426, headers: { Upgrade: 'websocket' } });
26
+ }
27
+ const Pair = globalThis.WebSocketPair;
28
+ if (!Pair)
29
+ throw new Error('@vrealtime/server/workers: upgrade() needs the Workers runtime (WebSocketPair)');
30
+ const pair = new Pair();
31
+ const client = pair[0];
32
+ const server = pair[1];
33
+ server.accept();
34
+ hubFor(agent, options).attach(server, { headers: request.headers });
35
+ return new Response(null, { status: 101, webSocket: client });
36
+ }
37
+ export { Hub };
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
2
  "name": "@vrealtime/server",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "vRealTime server: attach any WebSocket, Node serve(), Cloudflare Workers and Durable Objects",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/austinchennn/vRT.git",
9
+ "directory": "packages/server"
10
+ },
11
+ "homepage": "https://github.com/austinchennn/vRT#readme",
12
+ "bugs": "https://github.com/austinchennn/vRT/issues",
13
+ "keywords": [
14
+ "realtime",
15
+ "voice",
16
+ "agent",
17
+ "websocket",
18
+ "vrealtime"
19
+ ],
20
+ "type": "module",
21
+ "sideEffects": false,
22
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "@vrealtime/source": "./src/index.ts",
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.js"
28
+ },
29
+ "./node": {
30
+ "@vrealtime/source": "./src/node/index.ts",
31
+ "types": "./dist/node/index.d.ts",
32
+ "default": "./dist/node/index.js"
33
+ },
34
+ "./workers": {
35
+ "@vrealtime/source": "./src/workers/index.ts",
36
+ "types": "./dist/workers/index.d.ts",
37
+ "default": "./dist/workers/index.js"
38
+ }
39
+ },
40
+ "files": [
41
+ "dist"
42
+ ],
43
+ "engines": {
44
+ "node": ">=22"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "scripts": {
50
+ "build": "tsc -p tsconfig.build.json"
51
+ },
52
+ "dependencies": {
53
+ "@vrealtime/core": "0.2.0",
54
+ "@vrealtime/protocol": "0.2.0"
55
+ },
56
+ "peerDependencies": {
57
+ "ws": "^8.0.0"
58
+ },
59
+ "peerDependenciesMeta": {
60
+ "ws": {
61
+ "optional": true
62
+ }
63
+ }
64
+ }