-
Notifications
You must be signed in to change notification settings - Fork 3
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
Standardize JavaScript Functions API #44
Comments
To start filling this out, we should get a comparison of how a simple hello world looks across various platforms. For workerd/cloudflare workers, we have two API models (one legacy and one preferred that we're actively moving to): Legacy (service worker style): // global addEventListener
addEventListener('fetch', (event) => {
// request is a property on event (e.g. request.event), as is `waitUntil`
// bindings (additional resources/capabilities configured for the worker) are injected as globals
event.respondWith(new Response("Hello World"));
}); New (ESM worker style): export default {
async fetch(request, env, context) {
// bindings are available through `env`, `waitUntil` is available on `context`
return new Response("Hello World");
}
} We use Error reporting is pretty straightforward. There's really nothing fancy. We throw synchronous errors and report unhandled promise rejections to the Overall, our strong preference is to stick with the ESM worker style as experience has shown us time and again that the service worker model is pretty limited. The Where things are going to get complicated here is that in any function more complicated than a simple "Hello World", there are most likely a number of vendor/platform specific additional APIs in use (e.g. /cc'ing @kentonv and @harrishancock for visibility. |
I think you can get really far with only Request and Response. Perhaps the standard should focus on that part first. Using your ESM example: export default {
async fetch(request) {
return new Response("Hello World");
}
} Using an example from Vercel: export const config = { runtime: 'edge' };
export default (request) => {
return new Response(`Hello, from ${request.url}`);
}; These look really similar with a slightly different export. |
To add another example, the syntax for Lagon is similar to Vercel, except that the function is a named export: export function handler(request) {
return new Response("Hello World");
} Logging is the same (using |
The syntax for Netlify Edge Functions is very similar to Vercel and Lagon, and both of those examples would work unchanged on Netlify. This is the standard signature for Netlify: export default async function handler(request: Request, context: Context) {
return new Response("Hello world")
} The Netlify also supports an optional We're open to adding more fields to |
For completeness, here is the current syntax we use at Red Hat for a "normal" function /**
* Your HTTP handling function, invoked with each request. This is an example
* function that echoes its input to the caller, and returns an error if
* the incoming request is something other than an HTTP POST or GET.
*
* In can be invoked with 'func invoke'
* It can be tested with 'npm test'
*
* @param {Context} context a context object.
* @param {object} context.body the request body if any
* @param {object} context.query the query string deserialized as an object, if any
* @param {object} context.log logging object with methods for 'info', 'warn', 'error', etc.
* @param {object} context.headers the HTTP request headers
* @param {string} context.method the HTTP request method
* @param {string} context.httpVersion the HTTP protocol version
* See: https://github.com/knative-sandbox/kn-plugin-func/blob/main/docs/guides/nodejs.md#the-context-object
*/
const handle = async (context) => {
// YOUR CODE HERE
context.log.info(JSON.stringify(context, null, 2));
// If the request is an HTTP POST, the context will contain the request body
if (context.method === 'POST') {
return {
body: context.body,
}
// If the request is an HTTP GET, the context will include a query string, if it exists
} else if (context.method === 'GET') {
return {
query: context.query,
}
} else {
return { statusCode: 405, statusMessage: 'Method not allowed' };
}
}
// Export the function
module.exports = { handle }; The only parameter here is the context object, which provides a few pieces of information If you need a function that can also handle CloudEvents, then an extra const { CloudEvent, HTTP } = require('cloudevents');
/**
* Your CloudEvent handling function, invoked with each request.
* This example function logs its input, and responds with a CloudEvent
* which echoes the incoming event data
*
* It can be invoked with 'func invoke'
* It can be tested with 'npm test'
*
* @param {Context} context a context object.
* @param {object} context.body the request body if any
* @param {object} context.query the query string deserialzed as an object, if any
* @param {object} context.log logging object with methods for 'info', 'warn', 'error', etc.
* @param {object} context.headers the HTTP request headers
* @param {string} context.method the HTTP request method
* @param {string} context.httpVersion the HTTP protocol version
* See: https://github.com/knative-sandbox/kn-plugin-func/blob/main/docs/guides/nodejs.md#the-context-object
* @param {CloudEvent} event the CloudEvent
*/
const handle = async (context, event) => {
// YOUR CODE HERE
context.log.info("context");
context.log.info(JSON.stringify(context, null, 2));
context.log.info("event");
context.log.info(JSON.stringify(event, null, 2));
return HTTP.binary(new CloudEvent({
source: 'event.handler',
type: 'echo',
data: event
}));
};
module.exports = { handle }; |
I guess there are a few main ways Cloudflare's interface is unusual here. Let me try to explain our reasoning.
|
Some notes on the AWS model that I took a little while back. AWS Function input/output model:From: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html function signatures are:
Parameters are:
For asynchronous function handlers, you return a response, error, or promise to the runtime instead of using callback. Response is object must be compatible with JSON.stringify as JSON is returned LoggingLogging uses console.log, err Error handlinghttps://docs.aws.amazon.com/lambda/latest/dg/nodejs-exceptions.html |
Reading through some of the descriptions they seem to assume http as the request/response. As mentioned for cloudflare workers I think we need an API which supports other types of events for functions as well (not that the APIs described can't do that, just not sure if that was considered). Ideally the function itself would not need to know if an event was from HTTP, pub/sub or something else, all of that would be up to the serverless framework to handle. The function would get an event in an expected format and generate a result in an expected format. The plubing and configuration that gets the event to the function and the result to the right place (next function, back to user etc) would be part of the specific setup for the platform on which the function was running. I think the AWS model is effectively JSON in, JSON out which I think supports that model. The data received could have fields which indicates what kind of request it is if that is necessary, and that could be used to return the result in a specific way but at the highest level the API might not need to be tied to that. We could then define some specific in/out formats as a second level if that is helpfull (for example http requests/responses) |
@mhdawson HTTP isn't necessarily just request/response, though. The request and response bodies are streams, and the streaming nature is extremely important for a lot of use cases, such as proxying, SSR, etc. And then there's WebSockets. I don't see how these could be represented using plain JSON -- you really need an API designed around the specific shape of the protocol. |
@kentonv I'm sure you understand the use cases/current implementations much better than me. I'll have to read up more/learn more about those use cases in the functions context to understand better. I can understand that streaming might need a different API but still wonder if it needs to be specific to the protocol versus class of protocols (for example an API for request/response, one for streaming, etc.) |
Unfortunately I don't think that's practical. Each of our exported events share the |
I agree on focusing on the http case first as I think that is the most common, but keeping in mind that other types need to be supported as well. |
From discussion in the meeting today next step is to
|
I've created the repo here: https://github.com/nodeshift/js-functions-standardization Nothing added yet |
Added an implementations sections to the repo: https://github.com/nodeshift/js-functions-standardization/tree/main/docs/implementations These were basically just a copy/paste of the above comments into each platform |
Problem
Today every competing JavaScript functions API is different enough that we end up with lower developer productivity and vendor lock-in. Lower developer productivity because developers have to learn multiple ways to write the JavaScript function code and may have to write more complex code to avoid vendor lock-in. Organizations wanting to leverage multiple functions providers or move from one provider to another incur significant additional cost.
Goal
Goal is to define a JavaScript functions API that avoids vendor lock-in and facilitates developer productivity while being general enough to allow different implementations and optimizations behind the scenes.
Things we should try to standardize
Things we should not try to standardize
Scenarios
Scenario 1: Writing a HelloWorld function in one vendors platform and moving to other vendors platforms
Shouldn’t have to change the code
Build steps and or configuration may have to change
Scenario 2: Writing in one vendor and moving to another (ex using Google Cloud Functions and move to Cloudflare workers)
If code does not use vendor specific APIs you should not have to change the code.
If code uses vendor specific APIs, you may need to change the code if you also need to use a different vendor for those calls.
Build steps and or configuration may have to change
Originally authored by @lholmquist
The text was updated successfully, but these errors were encountered: