Skip to content
This repository was archived by the owner on Oct 22, 2025. It is now read-only.

Commit 361980e

Browse files
committed
chore: implement inline client for all drivers
1 parent fd29304 commit 361980e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+3042
-1221
lines changed

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@
170170
"@types/invariant": "^2",
171171
"@types/node": "^22.13.1",
172172
"@types/ws": "^8",
173+
"@vitest/ui": "3.1.1",
173174
"bundle-require": "^5.1.0",
174175
"eventsource": "^3.0.5",
175176
"tsup": "^8.4.0",

packages/core/scripts/dump-openapi.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createManagerRouter } from "@/manager/router";
2-
import { AppConfig, AppConfigSchema, setup } from "@/mod";
2+
import { AppConfig, AppConfigSchema, Encoding, setup } from "@/mod";
33
import { ConnectionHandlers } from "@/worker/router-endpoints";
44
import { DriverConfig } from "@/driver-helpers/config";
55
import {
@@ -11,6 +11,11 @@ import { OpenAPIHono } from "@hono/zod-openapi";
1111
import { VERSION } from "@/utils";
1212
import * as fs from "node:fs/promises";
1313
import { resolve } from "node:path";
14+
import { ClientDriver } from "@/client/client";
15+
import { WorkerQuery } from "@/manager/protocol/query";
16+
import { ToServer } from "@/worker/protocol/message/to-server";
17+
import { EventSource } from "eventsource";
18+
import { Context } from "hono";
1419

1520
function main() {
1621
const appConfig: AppConfig = AppConfigSchema.parse({ workers: {} });
@@ -40,13 +45,26 @@ function main() {
4045
},
4146
};
4247

43-
const managerRouter = createManagerRouter(appConfig, driverConfig, {
44-
routingHandler: {
45-
inline: {
46-
handlers: sharedConnectionHandlers,
48+
const inlineClientDriver: ClientDriver = {
49+
action: unimplemented,
50+
resolveWorkerId: unimplemented,
51+
connectWebSocket: unimplemented,
52+
connectSse: unimplemented,
53+
sendHttpMessage: unimplemented,
54+
};
55+
56+
const managerRouter = createManagerRouter(
57+
appConfig,
58+
driverConfig,
59+
inlineClientDriver,
60+
{
61+
routingHandler: {
62+
inline: {
63+
handlers: sharedConnectionHandlers,
64+
},
4765
},
4866
},
49-
}) as unknown as OpenAPIHono;
67+
) as unknown as OpenAPIHono;
5068

5169
const openApiDoc = managerRouter.getOpenAPIDocument({
5270
openapi: "3.0.0",

packages/core/src/app/config.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { z } from "zod";
44
import type { cors } from "hono/cors";
55
import { WorkerDefinition, AnyWorkerDefinition } from "@/worker/definition";
66
import { InspectorConfigSchema } from "@/inspector/config";
7-
87
// Define CORS options schema
98
type CorsOptions = NonNullable<Parameters<typeof cors>[0]>;
109

@@ -48,6 +47,9 @@ export const WorkersSchema = z.record(
4847
);
4948
export type Workers = z.infer<typeof WorkersSchema>;
5049

50+
export const TestConfigSchema = z.object({ enabled: z.boolean() });
51+
export type TestConfig = z.infer<typeof TestConfigSchema>;
52+
5153
/** Base config used for the worker config across all platforms. */
5254
export const AppConfigSchema = z.object({
5355
workers: z.record(z.string(), z.custom<AnyWorkerDefinition>()),
@@ -71,6 +73,14 @@ export const AppConfigSchema = z.object({
7173

7274
/** Inspector configuration. */
7375
inspector: InspectorConfigSchema.optional().default({ enabled: false }),
76+
77+
// TODO: Find a better way of passing around the test config
78+
/**
79+
* Test configuration.
80+
*
81+
* DO NOT MANUALLY ENABLE. THIS IS USED INTERNALLY.
82+
**/
83+
test: TestConfigSchema.optional().default({ enabled: false }),
7484
});
7585
export type AppConfig = z.infer<typeof AppConfigSchema>;
7686
export type AppConfigInput<A extends Workers> = Omit<
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import { logger } from "./log";
2+
import type { SSEStreamingApi } from "hono/streaming";
3+
import type { EventSource } from "eventsource";
4+
5+
/**
6+
* FakeEventSource provides a minimal implementation of an SSE stream
7+
* that handles events for the inline client driver
8+
*/
9+
export class FakeEventSource {
10+
url = "http://internal-sse-endpoint";
11+
readyState = 1; // OPEN
12+
withCredentials = false;
13+
14+
// Event handlers
15+
onopen: ((this: EventSource, ev: Event) => any) | null = null;
16+
onmessage: ((this: EventSource, ev: MessageEvent) => any) | null = null;
17+
onerror: ((this: EventSource, ev: Event) => any) | null = null;
18+
19+
// Private event listeners
20+
#listeners: Record<string, Set<EventListener>> = {
21+
open: new Set(),
22+
message: new Set(),
23+
error: new Set(),
24+
close: new Set()
25+
};
26+
27+
// Stream that will be passed to the handler
28+
#stream: SSEStreamingApi;
29+
#onCloseCallback: () => Promise<void>;
30+
31+
/**
32+
* Creates a new FakeEventSource
33+
*/
34+
constructor(onCloseCallback: () => Promise<void>) {
35+
this.#onCloseCallback = onCloseCallback;
36+
37+
this.#stream = this.#createStreamApi();
38+
39+
// Trigger open event on next tick
40+
setTimeout(() => {
41+
if (this.readyState === 1) {
42+
this.#dispatchEvent('open');
43+
}
44+
}, 0);
45+
46+
logger().debug("FakeEventSource created");
47+
}
48+
49+
// Creates the SSE streaming API implementation
50+
#createStreamApi(): SSEStreamingApi {
51+
// Create self-reference for closures
52+
const self = this;
53+
54+
const streamApi: SSEStreamingApi = {
55+
write: async (input) => {
56+
const data = typeof input === "string" ? input : new TextDecoder().decode(input);
57+
self.#dispatchEvent('message', { data });
58+
return streamApi;
59+
},
60+
61+
writeln: async (input: string) => {
62+
await streamApi.write(input + "\n");
63+
return streamApi;
64+
},
65+
66+
writeSSE: async (message: { data: string | Promise<string>, event?: string, id?: string, retry?: number }): Promise<void> => {
67+
const data = await message.data;
68+
69+
if (message.event) {
70+
self.#dispatchEvent(message.event, { data });
71+
} else {
72+
self.#dispatchEvent('message', { data });
73+
}
74+
},
75+
76+
sleep: async (ms: number) => {
77+
await new Promise(resolve => setTimeout(resolve, ms));
78+
return streamApi;
79+
},
80+
81+
close: async () => {
82+
self.close();
83+
},
84+
85+
pipe: async (_body: ReadableStream) => {
86+
// No-op implementation
87+
},
88+
89+
onAbort: async (cb: () => void) => {
90+
self.addEventListener("error", () => {
91+
cb();
92+
});
93+
return streamApi;
94+
},
95+
96+
abort: async () => {
97+
self.#dispatchEvent('error');
98+
return streamApi;
99+
},
100+
101+
// Additional required properties
102+
get responseReadable() {
103+
return null as unknown as ReadableStream;
104+
},
105+
106+
get aborted() {
107+
return self.readyState === 2; // CLOSED
108+
},
109+
110+
get closed() {
111+
return self.readyState === 2; // CLOSED
112+
}
113+
};
114+
115+
return streamApi;
116+
}
117+
118+
/**
119+
* Closes the connection
120+
*/
121+
close(): void {
122+
if (this.readyState === 2) { // CLOSED
123+
return;
124+
}
125+
126+
logger().debug("closing FakeEventSource");
127+
this.readyState = 2; // CLOSED
128+
129+
// Call the close callback
130+
this.#onCloseCallback().catch(err => {
131+
logger().error("error in onClose callback", { error: err });
132+
});
133+
134+
// Dispatch close event
135+
this.#dispatchEvent('close');
136+
}
137+
138+
/**
139+
* Get the stream API to pass to the handler
140+
*/
141+
getStream(): SSEStreamingApi {
142+
return this.#stream;
143+
}
144+
145+
// Implementation of EventTarget-like interface
146+
addEventListener(type: string, listener: EventListener): void {
147+
if (!this.#listeners[type]) {
148+
this.#listeners[type] = new Set();
149+
}
150+
this.#listeners[type].add(listener);
151+
152+
// Map to onX properties as well
153+
if (type === "open" && typeof listener === "function" && !this.onopen) {
154+
this.onopen = listener as any;
155+
} else if (type === "message" && typeof listener === "function" && !this.onmessage) {
156+
this.onmessage = listener as any;
157+
} else if (type === "error" && typeof listener === "function" && !this.onerror) {
158+
this.onerror = listener as any;
159+
}
160+
}
161+
162+
removeEventListener(type: string, listener: EventListener): void {
163+
if (this.#listeners[type]) {
164+
this.#listeners[type].delete(listener);
165+
}
166+
167+
// Unset onX property if it matches
168+
if (type === "open" && this.onopen === listener) {
169+
this.onopen = null;
170+
} else if (type === "message" && this.onmessage === listener) {
171+
this.onmessage = null;
172+
} else if (type === "error" && this.onerror === listener) {
173+
this.onerror = null;
174+
}
175+
}
176+
177+
// Internal method to dispatch events
178+
#dispatchEvent(type: string, detail?: Record<string, any>): void {
179+
// Create appropriate event
180+
let event: Event;
181+
if (type === 'message' || detail) {
182+
event = new MessageEvent(type, detail);
183+
} else {
184+
event = new Event(type);
185+
}
186+
187+
// Call specific handler
188+
if (type === 'open' && this.onopen) {
189+
try {
190+
this.onopen.call(this as any, event);
191+
} catch (err) {
192+
logger().error("error in onopen handler", { error: err });
193+
}
194+
} else if (type === 'message' && this.onmessage) {
195+
try {
196+
this.onmessage.call(this as any, event as MessageEvent);
197+
} catch (err) {
198+
logger().error("error in onmessage handler", { error: err });
199+
}
200+
} else if (type === 'error' && this.onerror) {
201+
try {
202+
this.onerror.call(this as any, event);
203+
} catch (err) {
204+
logger().error("error in onerror handler", { error: err });
205+
}
206+
}
207+
208+
// Call all listeners
209+
if (this.#listeners[type]) {
210+
for (const listener of this.#listeners[type]) {
211+
try {
212+
listener.call(this, event);
213+
} catch (err) {
214+
logger().error(`error in ${type} event listener`, { error: err });
215+
}
216+
}
217+
}
218+
}
219+
}

0 commit comments

Comments
 (0)