-
Notifications
You must be signed in to change notification settings - Fork 0
[ECO-5260] feat: initial local proxy implementation #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
b21bea7
to
3323ede
Compare
3a2342b
to
3c6051e
Compare
Core functionality includes: - Spying on HTTP Requests and Responses - Inspect outgoing HTTP requests from the client. - Inspect and modify incoming HTTP responses. - Spying on WebSocket Messages - Observe outgoing WebSocket messages from the client. - Inspect and modify incoming WebSocket messages. - Injecting WebSocket Messages - Simulate server messages by injecting WebSocket frames into the client session. - Breaking WebSocket and HTTP Connections - Force WebSocket disconnections to test reconnection behavior. - Simulate slow or broken HTTP responses to test timeout handling.
3c6051e
to
dd96ae9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks interesting!
Observing and injecting APIs by itself should enable replacing a significant amount of private API usage in LiveObjects tests, as these tests often either waited for a specific message to arrive over WebSocket or injected a bunch of messages to emulate server behavior.
I've added some comments in the code below, along with more general suggestions here:
- Either use the explicit
public
modifier for public fields everywhere, or omit it everywhere. Currently, it's mixed - seepublic async start()
andpauseAllConnections(): () => void
. - Regarding naming for proxy public methods: I imagine that methods using the
observe*
naming convention would accept a callback to continuously notify the caller about events (i.e., actively observing something). However, this is not the case with the current public API, whereobserve*
methods just return the next encountered request or message. Should we rename them to something more better suited, such aswaitForNextRequest
,waitForNextOutgoingProtocolMessage
, or even dropping thewaitFor
prefix: e.g.,proxy.nextRequest()
orproxy.nextOutgoingProtocolMessage()
?
await observerPromise; | ||
} | ||
|
||
export async function openConnection(ws: WebSocket) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
probably worth renaming to waitForConnectionOpen
to keep parity with other methods below
|
||
class DefaultCompletableDeferred<T = void> { | ||
private _completeWith!: CompletionHandler<T>; | ||
private value: T | undefined |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it look like value
is never set? is it still needed?
@@ -0,0 +1,22 @@ | |||
type CompletionHandler<T = void> = (value: T) => void | |||
|
|||
class DefaultCompletableDeferred<T = void> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would like to have a description for this primitive as it's not immediately obvious what it does.
was it by any chance copied from somewhere? maybe can include a link to source too
try { | ||
await task(); | ||
} finally { | ||
deferredValue.complete(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we're not catching an error here and I believe it will lead to undandled promise rejections at the line below where we call this.processNext()
(as that will throw an error when executin task).
is it intentional here?
also not handling an error prevents us from passing the error to the deferredValue - caller of the async-queue can't know if it failed
const task = this.queue.shift(); | ||
|
||
try { | ||
await task!!(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
await task!!(); | |
await task!(); |
should it be just one exclamation mark?
proxy.dropConnection(client.connection.id); | ||
|
||
// Pause and resume connection manually | ||
const resume = proxy.pauseConnection(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pauseConnection
is not in the InterceptingProxy interface. should it be pauseAllConnections
?
that being said we should still have a way to pause connection by an id. wdyt?
async () => testWs.send(JSON.stringify({ action: 1 })), | ||
async () => { | ||
const message = await proxy.observeNextIncomingProtocolMessage(); | ||
const rawWebsocketMessage = await waitForNextReceivedMessage(testWs); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can these two message be different somehow? why do we need to check both?
private readonly proxy = httpProxy.createProxyServer({}); | ||
private readonly wss = new WebSocketServer({ noServer: true }); | ||
|
||
private readonly server: http.Server = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be worth extracting this createServer logic and similar https://github.com/ably-labs/local-proxy-js/pull/2/files#diff-78bb005ca752086400a648d8aa3e60ce6f666e213da508caabd464d3bfeaf6a2R235 and https://github.com/ably-labs/local-proxy-js/pull/2/files#diff-78bb005ca752086400a648d8aa3e60ce6f666e213da508caabd464d3bfeaf6a2R253 for websocket server to separate functions/classes with a bit more comments to explain what's going on in them.
As far I as I can see these are the parts that intercept http and ws traffic , so we can call them InterceptingHttpServer and InterceptingWebSocketServer
const deferredValue = CompletableDeferred<Request<T>>(); | ||
const unregister = this.registerRestMiddleware(async (req, res) => { | ||
if (!filter || filter(req)) deferredValue.complete(req); | ||
return res; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return res; | |
return req; |
shouldn't it return request?
Initial local proxy implementation
Core functionality includes:
Spying on HTTP Requests and Responses
Spying on WebSocket Messages
Injecting WebSocket Messages
Breaking WebSocket and HTTP Connections