Skip to content
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

feat: add more robust remote transport to communicate between the runner and server #16449

Conversation

sheremet-va
Copy link
Member

@sheremet-va sheremet-va commented Apr 18, 2024

Description

This PR updates two primitives introduced not so long ago. By default, you can still just invoke RemoteRunnerTransport and RemoteEnvironmentTransport when needed.

import { RemoteRunnerTransport, ModuleRunner } from 'vite/module-runner'

// this is defined in the runner context
const transport = new RemoteRunnerTransport({ send, onMessage })
// auto registers, now you can call environment.evaluate(url) on the server side
const runner = new ModuleRunner({ transport })
import { RemoteEnvironmentTransport, DevEnvironment } from 'vite'

// this is defined on the server
const transport = new RemoteEnvironmentTransport({ send, onMessage })
const environment = new DevEnvironment(server, 'worker', { runner: { transport } }) // auto registers

// new API: "evaluate" - this will communicate with initiated remote runner transport
// before calling this, make sure that communication is established
// this method just evaluates the module, but doesn't return the module itself
await environment.evaluate('/remote-url.js')

The cool new thing is that you can define your own methods to communicate between the runner and the server:

import { RemoteRunnerTransport } from 'vite/module-runner'

const transport = new RemoteRunnerTransport<
  { doSomethingOnTheRunnerSide: () => Magic }
>({ 
  send, 
  onMessage,
  methods: {
    doSomethingOnTheRunnerSide() {
      // do something
      return { magic: true }
    }
  }
})
import { RemoteEnvironmentTransport, DevEnvironment } from 'vite'

const transport = new RemoteEnvironmentTransport<
  {}, 
  { doSomethingOnTheRunnerSide: () => Magic }
>({ send, onMessage })
const result = await transport.invoke('doSomethingOnTheRunnerSide')
// result => { magic: true }

The transport also returns the value unserialised, so you need to make sure that your communication channel supports sending this type of data. For the typescript usage, you can also reuse the same interface for methods/events (methods on one side will equal to events on the other).

You can also do the same the other way around:

import { RemoteRunnerTransport } from 'vite/module-runner'

const transport = new RemoteRunnerTransport<
  {}, 
  { doSomethingOnTheServer: () => Magic }
>({ send, onMessage })
const result = await transport.invoke('doSomethingOnTheServer')
// result => { magic: true }
import { RemoteEnvironmentTransport, DevEnvironment } from 'vite'

const transport = new RemoteEnvironmentTransport<
  { doSomethingOnTheServer: () => Magic }
>({ 
  send, 
  onMessage,
  methods: {
    doSomethingOnTheServer() {
      // do something
      return { magic: true }
    }
  }
})

TODO:

  • Docs

Copy link

stackblitz bot commented Apr 18, 2024

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@sheremet-va
Copy link
Member Author

sheremet-va commented Apr 18, 2024

@hi-ogawa @patak-dev would love to hear your feedback about the API

@hi-ogawa
Copy link
Collaborator

I was also thinking that having evaluate like features out-of-the-box might be nice, but while iterating on the ideas and use cases, I'm not entirely sure if it adds so much value to have this officially.

Like Anthony suggested initially, I'm leaning towards holding off designing the official API for this until more experiments from frameworks start.

In my opinion, evaluate: (url: string) => void for remote runner wouldn't be so useful and if users need that, environment can probably easily implement it.

To make evaluate somewhat usable, I was thinking about the signature of the form like playwright does for browser code execution from test runner https://playwright.dev/docs/evaluating

{
  evaluate: <Args, Ret>(entry: string, fn: (mod: any, ...args: Args) => Ret, ...args: Args) => Promise<Awaited<Ret>>;
}

With this form, we don't have to think about serializing entire mod: any since only fn.toString and Args needs to be transported and evaluated on remote runner then Ret will be transported back. But this is still not powerful enough for the actual use cases like Astro since it looks like they have own ModuleLoader.import abstraction with ssrLoadModule and this import is used to load entire middleware handler etc... https://github.com/withastro/astro/blob/7fda037b277201b261e53dd0d43a92c5e8f1c638/packages/astro/src/core/middleware/loadMiddleware.ts#L13

Another concern I had with evaluate API design is that, in my workerd example case, evaluate wouldn't be probably useful unless I expose workerd specific binding object env. So what I ended up looks like this:
https://github.com/hi-ogawa/vite-environment-examples/blob/f2d4ab79d9c250d70273d9d3d13b9997f88354e0/packages/workerd/src/shared.ts#L22-L33

export type EvalApi = <In = any, Out = any>(request: {
  entry: string;
  data?: In;
  fn: (ctx: { mod: any; data?: In; env: any; runner: ModuleRunner; }) => Out;
}) => Promise<Awaited<Out>>;

In the end, I thought this is an environment specific choice, so providing evaluate from Vite might not be a good fit.

@sheremet-va
Copy link
Member Author

One of the use cases was from @pi0 - the idea is to use this method to run tests inside of these environments directly. Something like Vitest calling environment.evaluate('vitest/dist/entry.js?test=file.js'). I am not sure how realistic it is, but might be worth investigating.

You didn't provide feedback on another part of the proposal - the invoke method to define methods/events on both sides and communicate between them easily. What are your thoughts on that?

@hi-ogawa
Copy link
Collaborator

hi-ogawa commented Apr 19, 2024

One of the use cases was from @pi0 - the idea is to use this method to run tests inside of these environments directly. Something like Vitest calling environment.evaluate('vitest/dist/entry.js?test=file.js'). I am not sure how realistic it is, but might be worth investigating.

Yeah, it would be certainly convenient, but I think it's more or less straight forward to implement if users have remote runner setup already, so I wasn't sure if officially exposing this API would change the usability.

This is related to the 2nd point, but if Vite provides birpc like system (or users setup own birpc), then can users do this with more flexibility?

{
  myEvaluate(entry: string, test: string) {
    const mod = await runner.import(entry);
    return mod.doSomething(test);
  }
}

You didn't provide feedback on another part of the proposal - the invoke method to define methods/events on both sides and communicate between them easily. What are your thoughts on that?

This is also nice, but since it's targeted for remote runner use cases, I feel it doesn't have to be implemented in core. Users (meaning small numbers of people who write remote runner integration) can setup birpc on their own or anything fits there needs.

I don't know if this is because I'm biased with Miniflare/Workerd scenario, but it just happens that their websocket isn't usable for fetchModule's huge payload, so it has to be workarounded in some way already (so, anything Vite provides for remote transport utility is not directly usable).
Also, since fetchModule alone can work in uni-directional communication, bi-directional send/onMessage interface is not strictly necessary to make remote runner work. In workerd case, it also just happens that I can use simpler service binding fetch to implement fetchModule in a uni-directional manner.

Due to this bias, I thought the value of this feature would be limited even for remote runner cases. And thus, I was suggesting to hold off a bit until people do more concrete experimentation (and struggle) with current API.

@patak-dev
Copy link
Member

I think we should continue to explore and create these draft PRs even if we end up delaying merging them until getting more millage with the API. These are very useful to have something concrete to discuss about. My take is that we should add an abstraction at one point, or utilities to create them so (at least between most environments) there is a standard and the ecosystem doesn't diverge in every direction.

@hi-ogawa
Copy link
Collaborator

I said hold-off, but I think it's totally fine to merge it to alpha branch since simpler version of RemoteEnvironmentTransport/RemoteRunnerTransport pair is already there.

I just didn't know who is testing alpha currently, but if it helps pi0 or anyone to integrate and experiment it faster, then providing this version makes sense.

@patak-dev patak-dev deleted the branch vitejs:feat/environment-api April 19, 2024 12:42
@patak-dev patak-dev closed this Apr 19, 2024
@patak-dev
Copy link
Member

Oh, @sheremet-va, just in case, I just saw that this PR was automatically closed when I merged the feat/environment-api branch.

@sheremet-va sheremet-va deleted the feat/environment-remote-transport branch April 30, 2024 09:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants