Conversation
🦋 Changeset detectedLatest commit: 334bfa0 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
a179b91 to
51f8854
Compare
| "prepublishOnly": "SOURCEMAPS=false npm run build", | ||
| "start": "pnpm run bundle && cross-env NODE_OPTIONS=--enable-source-maps ./bin/wrangler.js", | ||
| "test": "pnpm run assert-git-version && jest", | ||
| "test": "pnpm run assert-git-version && jest --runInBand", |
There was a problem hiding this comment.
Had to do this to avoid a race condition where every test thought it could start the InspectorProxyWorker on 9229 because get-port doesn't work across processes (jest workers, in this case)
We confirmed this affects wrangler dev and unstable_dev if you run multiple processes in parallel where get-port thinks the requested port is available but then there's a race for all the workerd instances to bind to it
We have a fix coming in a follow-up
There was a problem hiding this comment.
TODO: make miniflare surface "port in use" errors so wrangler can retry instantiating Miniflare with port 0
| export interface UnstableDevWorker { | ||
| port: number; | ||
| address: string; | ||
| proxyData: ProxyData; |
There was a problem hiding this comment.
Adding to this public interface so we can fake some startDevWorker events (call the event listeners)
There was a problem hiding this comment.
TODO add getBindings to this interface
| /** | ||
| * @internal | ||
| */ | ||
| export class DevEnv extends EventEmitter { |
There was a problem hiding this comment.
This class contains the glue code for all startDevWorker controllers. This is the contract between all controllers. You could override the instances of each/any controller in the constructor but we're not exposing this to users yet until we're more sure of the contract.
There was a problem hiding this comment.
First pass, tentatively, looks GREAT! I will probably go over it again maybe even jump on a call with @RamIdeas for a pair review for a 2nd pass!
This PR also should have a couple approvals minimum.
| @@ -0,0 +1,41 @@ | |||
| import type Protocol from "devtools-protocol/types/protocol-mapping"; | |||
There was a problem hiding this comment.
The types from devtools-protocol don't expose the full object we see in the websocket messages, just the sub-properties for some reason
This file may seem daunting but it just coerces the types into the whole object we see – e.g. with method and params matched together not as separate type unions which is basically useless
@mrbbot got nerd-sniped and got it working 👏
| @@ -244,7 +255,55 @@ type DevSessionProps = DevProps & { | |||
| }; | |||
|
|
|||
| function DevSession(props: DevSessionProps) { | |||
There was a problem hiding this comment.
This is (essentially) the entry-point to wrangler dev – here we create a DevEnv and map the props into StartDevWorkerOptions (at least what is relevant for now – for ProxyController) and fake events by calling ProxyController event handler methods (devEnv.proxy.on*)
| initialPort={0} // hard-code for userworker, DevEnv-ProxyWorker now uses this prop value | ||
| initialIp={"127.0.0.1"} // hard-code for userworker, DevEnv-ProxyWorker now uses this prop value | ||
| rules={props.rules} | ||
| inspectorPort={props.inspectorPort} | ||
| runtimeInspectorPort={props.runtimeInspectorPort} | ||
| localPersistencePath={props.localPersistencePath} | ||
| liveReload={props.liveReload} | ||
| crons={props.crons} | ||
| queueConsumers={props.queueConsumers} | ||
| localProtocol={props.localProtocol} | ||
| localProtocol={"http"} // hard-code for userworker, DevEnv-ProxyWorker now uses this prop value |
There was a problem hiding this comment.
The user worker now never starts on https, and binds to 127.0.0.1 on a random port. The ProxyWorker will start on the previous address values
| usePreviewServer({ | ||
| previewToken, | ||
| assetDirectory: props.isWorkersSite | ||
| ? undefined | ||
| : props.assetPaths?.assetDirectory, | ||
| localProtocol: props.localProtocol, | ||
| localPort: props.port, | ||
| ip: props.ip, | ||
| }); | ||
|
|
||
| useInspector({ | ||
| inspectorUrl: | ||
| props.inspect && previewToken | ||
| ? previewToken.inspectorUrl.href | ||
| : undefined, | ||
| port: props.inspectorPort, | ||
| logToTerminal: true, | ||
| sourceMapPath: props.sourceMapPath, | ||
| host: previewToken?.host, | ||
| name: props.name, | ||
| sourceMapMetadata: props.bundle?.sourceMapMetadata, | ||
| }); | ||
|
|
There was a problem hiding this comment.
No more preview server or inspector server, replaced by ProxyWorker and ProxyInspectorWorker respectively
| } | ||
| } | ||
|
|
||
| const devEnv = new DevEnv(); |
There was a problem hiding this comment.
This outer function (startDevServer) is called internally by unstable_dev. Here we create a DevEnv and fake events to the ProxyController by calling it's event handler methods (devEnv.proxy.on*)
| // `selfsigned` imports `node-forge`, which is a pretty big library. | ||
| // To reduce startup time, only load this dynamically when needed. | ||
| // eslint-disable-next-line @typescript-eslint/consistent-type-imports | ||
| const generate: typeof import("selfsigned").generate = promisify( |
There was a problem hiding this comment.
This is one of those fake async libs where they provided a node.js callback signature for ecosystem consistency. It makes no difference to call it without a callback ("synchronously") but this way we can de-async the callstack
There was a problem hiding this comment.
Alternatively, could we take advantage of the existing async nature of this function to use an ESM await import()? Reducing synchronous require()s will be helpful with things like e.g. a future move to Viteste
There was a problem hiding this comment.
I think await import() segfaults in Jest, breaking our existing tests 🙁
| @@ -0,0 +1,4 @@ | |||
| declare module "worker:*" { | |||
There was a problem hiding this comment.
Declaring the type for worker: prefix module imports. We added an esbuild plugin to find these worker files in packages/wrangler/templates/*
| this.requestQueue.set(request, promise); | ||
| this.processQueue(); | ||
|
|
||
| return promise; |
There was a problem hiding this comment.
Synchronously returning this promise. The promise will resolve later when we process the queue.
There was a problem hiding this comment.
Any possible things running that would depend on this Promise? Considering possible race conditions or the like.
| processProxyControllerRequest(request: Request) { | ||
| const event = request.cf?.hostMetadata; | ||
| switch (event?.type) { | ||
| case "pause": |
There was a problem hiding this comment.
pause events cause the ProxyWorker to buffer requests
| this.proxyData = undefined; | ||
| break; | ||
|
|
||
| case "play": |
There was a problem hiding this comment.
play events cause the ProxyWorker to begin proxying requests to the UserWorker (using proxyData)
| await env.PROXY_CONTROLLER.fetch("http://dummy", { | ||
| method: "POST", | ||
| body: JSON.stringify(message), | ||
| }); |
There was a problem hiding this comment.
The ProxyWorker can communicate back to the ProxyController via this service binding
| } | ||
| } | ||
|
|
||
| void fetch(url, new Request(req, { headers })) |
There was a problem hiding this comment.
Not awaiting this fetch means we replay the requests in order but we don't guarantee their delivery in order. We could await just the headers (not the body) to guarantee delivery order too but I don't think it matters too much – happy to hear other opinions
There was a problem hiding this comment.
If you are chaining then & catch, why is the void necessary?
There was a problem hiding this comment.
It shouldn't be a floating promise or anything, if its been chained with a catch I mean.
There was a problem hiding this comment.
Just being explicit about not awaiting the promise since we're in a loop and it's on purpose
But I guess I could just add a comment instead
There was a problem hiding this comment.
Is there any reason not to await the fetch? We're already creating an ordered buffer of requests, we might as well make sure that the delivery order is preserved
There was a problem hiding this comment.
The main reason is that if the UserWorker has any long-running requests and therefore requires parallel request handling, await-ing would prevent that.
I've left a comment stating, if we choose to await, we should also race it against a timeout of e.g. 100ms
| res = insertLiveReloadScript(req, res, this.env, proxyData); | ||
| } | ||
|
|
||
| deferred.resolve(res); |
There was a problem hiding this comment.
This resolves the promise returned by the initial fetch handler
| }, | ||
| }); | ||
|
|
||
| deferred.reject(error); |
There was a problem hiding this comment.
This rejects the promise returned by the initial fetch handler
packages/wrangler/templates/startDevWorker/InspectorProxyWorker.ts
Outdated
Show resolved
Hide resolved
| }, | ||
| } as ExportedHandler<Env>; | ||
|
|
||
| export class InspectorProxyWorker implements DurableObject { |
There was a problem hiding this comment.
This is a big boi, sorry
It's job is to proxy messages between the runtime inspector (#websockets.runtime), ProxyController (#websockets.proxyController) and devtools client (#websockets.devtools).
Because only one client can be connected to the runtime inspector server, the InspectorProxyWorker is essentially that one client. The ProxyController and devtools then can both connect to the InspectorProxyWorker.
247cf2c to
8863445
Compare
for ProxyController -> (Inspector)ProxyWorker requests to aid debugging
without requiring ProxyWorker restart
|
|
||
| // if passed a previousDeferred, ensure it is resolved with the newDeferred | ||
| // so that await-ers of previousDeferred are now await-ing newDeferred | ||
| previousDeferred?.resolve(newPromise); |
There was a problem hiding this comment.
I'm a bit unclear on the use-case for chaining deferreds. If a deferred gets inherited, any holder of the previous deferred loses control over its lifecycle (the resolvers become inert since its promise has now already been resolved). Similarly, if the previous deferred has already been resolved before it gets inherited, the new deferred isn't allowed to extend the lifecycle of the previous one.
So whether or not a holder of a deferred maintains control over its lifecycle, and whether or not its lifecycle has been extended is unreliable. This seems error-prone and like it would lead to some insidious bugs.
There was a problem hiding this comment.
If a deferred gets inherited, any holder of the previous deferred loses control over its lifecycle (the resolvers become inert since its promise has now already been resolved)
True
Similarly, if the previous deferred has already been resolved before it gets inherited, the new deferred isn't allowed to extend the lifecycle of the previous one.
True
So whether or not a holder of a deferred maintains control over its lifecycle, and whether or not its lifecycle has been extended is unreliable. This seems error-prone and like it would lead to some insidious bugs.
The point is not to extend the lifecycle of the old promise. It's to ensure the old promise is not left uncompleted. If the old promise is already completed (resolved/rejected), this line has no effect
What this PR solves / how to test:
This PR implements the first milestone for
startDevWorker. This includes the ProxyController, ProxyWorker and ProxyInspectorWorker. ThestartDevWorkerfunction is not exposed yet – the components implemented so far have been inserted tactfully 👀 into the existing flows forwrangler dev,wrangler dev --remote,unstable_dev(...).There should be no breaking changes. Improvements include:
Author has included the following, where applicable:
Reviewer is to perform the following, as applicable:
Note for PR author:
We want to celebrate and highlight awesome PR review! If you think this PR received a particularly high-caliber review, please assign it the label
highlight pr reviewso future reviewers can take inspiration and learn from it.