lukeed / worktop Goto Github PK
View Code? Open in Web Editor NEWThe next generation web framework for Cloudflare Workers
License: MIT License
The next generation web framework for Cloudflare Workers
License: MIT License
When calling req.extend
, it triggers a 500 error with the message Illegal invocation
. I have tried using event.waitUntil
directly and it seems to work. Following is the relevant code, please let me know if I should provide any other information.
import { Router, listen } from 'worktop'
const router = new Router()
const handleTest = async (event) => {
event.waitUntil(fetch('https://cloudflare.com'))
return new Response('ok')
}
router.add('GET', '/test2', async (req, res) => {
req.extend(fetch('https://cloudflare.com'))
res.send(200, 'ok')
})
addEventListener('fetch', (event) => {
const { pathname } = new URL(event.request.url)
if (pathname === '/test') event.respondWith(handleTest(event))
else listen(router.run)
})
The domain I had in mind got sniped π’ which is actually the main reason a docs sites doesn't exist yet, haha
Right now, documentation mostly lives in/as the TypeScript definitions and the PR description(s) for new features. This is somewhat permissible for 0.x
days, but will need something more formal for 1.0 and beyond.
Just brainstorming; nothing may actually come out of this!
I've been looking at the ServerRequest
and ServerResponse
interfaces a lot lately & wondering if they should even be here π€
First, an overview of the PROs/CONs of everything, which lays out the reasoning for their existence:
ServerRequest
PROs
ServerRequest<P>
TS interface makes for really nice DX as it can give confidence wrt req.params
contentsreq.body()
"smart helper" is nice for auto-detecting & auto-parsing the bodyreq.body.json()
(and other) methods, if needednew URL
is done once and its values are stored on the request
directly, so don't have to repeatCONs
Request
to ServerRequest
req.clone
as it no longer existsnew Request(req.url, req)
out of the box (related #52)ws.connect
doesn't have a preferencews.listen
returns a handler than needs to be given a ServerRequest
because of the req.params
usage within itServerRequest
must be created from a FetchEvent
in order for req.extend
to exist
FetchEvent
sVisually
Compared to Request
, these are the property differences:
url: string;
++ path: string;
method: Method;
++ origin: string;
++ hostname: string;
++ search: string;
++ query: URLSearchParams;
++ extend: FetchEvent['waitUntil'];
cf: IncomingCloudflareProperties;
headers: Headers;
++ params: P;
-- json(): Promise<any>;
-- formData(): Promise<FormData>;
-- arrayBuffer(): Promise<ArrayBuffer>;
-- blob(): Promise<Blob>;
-- text(): Promise<string>;
++ body: {
++ <T>(): Promise<T|void>;
++ json<T=any>(): Promise<T>;
++ arrayBuffer(): Promise<ArrayBuffer>;
++ formData(): Promise<FormData>;
++ text(): Promise<string>;
++ blob(): Promise<Blob>;
++ };
-- cache: RequestCache;
-- credentials: RequestCredentials;
-- destination: RequestDestination;
-- integrity: string;
-- keepalive: boolean;
-- mode: RequestMode;
-- redirect: RequestRedirect;
-- referrer: string;
-- referrerPolicy: ReferrerPolicy'
-- signal: AbortSignal;
-- clone(): Request;
ServerResponse
The main purpose of ServerResponse
was to surface a Node.js-like API for composing responses.
PROs
res.end
, res.setHeader
, res.writeHead
and a bunch of othersnew Response(res.body, res)
for ServerResponse
-> Response
transformres.end
or res.send
is calledreq.method
for construction β used for HEAD
checksres.send
for automatic Content-*
headers and res.body
serializationCONs
Response
type requirements
ServerResponse
to an external library/helper if it wants a Response
valueServerResponse
is tiny, it's still extra codeHandler
The Handler
right now is strictly tied to the ServerRequest, ServerResponse
pair. It makes sense for this to always have some worktop-specific signature to it, but the question boils down to whether or not ServerRequest
and ServerResponse
are the right base units.
The signature now is this:
type Handler<P> = (req: ServerRequest<P>, res: ServerResponse) => Promisable<Response|void>;
...which satisfies all "middleware" and "final/route handler" requirements. Worktop loops through all route handlers until either:
Response
or Promise<Response>
is returned directlyres.send
or res.end
have been called, which marks the ServerResponse
as finished, and a Response
is created from the ServerResponse
contentsPROs
next()
CONs
ServerRequest
and ServerResponse
to be setup firstWith the background out of the way, I have a few ideas of how these could be simplified and/or reworked to be compatible with libraries outside of worktop.
Important: All of these suggestions are breaking changes, which is not taken lightly.
If any of these are to happen, Worktop would have a strong division between old-vs-new through versioning β supporting the existing API and {new stuff} would not be considered.
Request
and add $
object for all customizationsThis drops ServerRequest
and instead uses Request
directly, adding a $
property that has an object with all worktop extras. This would be like how Cloudflare adds the cf
key with its own metadata.
interface RequestExtras<P extends Params> {
params: P;
path: string;
origin: string;
hostname: string;
search: string;
query: URLSearchParams;
body<T>(): Promise<T | void>;
extend: FetchEvent['waitUntil'];
}
interface Request<P extends Params = Params> {
$: RequestExtras<P>;
}
Breaks:
req.params
-> req.$.params
req.path
-> req.$.path
req.*
-> req.$.*
req.body()
-> req.$.body()
req.body.json()
-> req.json()
(rely on native)req.body.*()
-> req.*()
(rely on native)req.extend()
-> req.$.extend()
Additionally, I see two potential issues with this:
Request
interface to a generic, which means code can throw/cause TS errors when used outside of worktop. In other words, if someone tries to do Request<MyParams>
outside of worktop, then that's gunna throw an error because Request
, natively, is not a generic. Not having the generic means that worktop loses its params
insights.req.$
always exists, but if a user writes a middleware/handler to be used outside of worktop, then something like req.$.path
will throw a cannot access "path" of undefined
errorRequest
and use context
object for all customizationsThis is the exact same thing as above, but it uses the context
key instead of $
for the object.
Request
objectUses the incoming Request
as is, but adds all the worktop customizations to it directly:
interface Request<P extends Params = Params> {
params: P;
path: string;
origin: string;
hostname: string;
search: string;
query: URLSearchParams;
extend: FetchEvent['waitUntil'];
}
Breaks:
req.body()
-> removed β use external utilityreq.body.json()
-> removed β use req.json()
req.body.*()
-> removed β use req.*()
Additionally, this shares the same potential gotchas as Options 1 & 2
Request
to be a generic; might cause issues externallyreq.params
object; any user middleware running outside of worktop might include req.params.foo
and req.params
is not definedThere's not much that can really change about ServerResponse
since it's all custom/worktop's to begin with... Really, there's only been on thing on my mind:
res.send
to external utilityIf res.send
were exported as a top-level send
utility (either from worktop/response
or from worktop/utils
), then people could use it externally.
In order to accomodate this change, send
would have to return a Response
directly. Right now it mutates the ServerResponse
to signal the worktop.Router
that the Handler loop is complete. All of the serialization & statusCode
checks would happen within the send
utility itself. However, it would be up to the Router
to perform the HEAD
check. This would keep its API more or less comparable to @polka/send
.
Note: It's entirely possible to keep
res.send
and add asend
export.
As mentioned before, the Handler
itself depends on decisions made to (Server)Request
and (Server)Response
. So any suggestions here will be made in addition to those, as this section is focusing on the Handler
function signature itself.
Important: For brevity, I'll use
Promise<Response>
as a return type to refer toPromisable<Response | void>
Keep the signature the same and use the exactly same ServerRequest
and ServerResponse
types:
type Handler<P> = (req: ServerRequest<P>, res: ServerResponse): Promise<Response>;
Request
typeMaintain the (req, res)
parameters, but replace ServerRequest
with one of the suggestions above.
type Handler<P> = (req: Request<P>, res: ServerResponse): Promise<Response>;
Request+
and a Context
objectNote: Using
Request+
to denote an overloaded/modifiedRequest
type; see above.
Changes the signature so that a Request
is always used, leaving it up to Context
to store additional/extra information. Your Handlers
will be passing around the same Context object, which allows middleware/handlers (including Router.prepare
) to mutate the context
as needed.
For this Option 3 entry, the Context
object looks like this:
type Context = {
response: ServerResponse;
waitUntil: FetchEvent['waitUntil'];
}
type Handler<
P extends Params = Params,
C extends Context = Context,
> = (req: Request<P>, context: C) => Promise<Response>;
// ^ this means you can inject your own `Context` types
Request
and a Context
objectSimilar to Option 3, but moves all would-be ServerRequest
extras into the Context
object. This means that the Request
is pure and has nothing added onto it (other than Cloudflare's cf
property)
type Context<P extends Params = Params> = {
// Request extras
params: P;
path: string;
origin: string;
hostname: string;
search: string;
query: URLSearchParams;
// the FetchEvent information
extend: FetchEvent['waitUntil']; // name TBD
waitUntil: FetchEvent['waitUntil']; // name TBD
passThroughOnException: FetchEvent['passThroughOnException']; // name TBD
// the ServerResponse
response: ServerResponse;
}
type Handler<
P extends Params = Params,
C extends Context = Context,
> = (req: Request, context: C<P>) => Promise<Response>;
// ^ this means you can inject your own `Context` types
Context
parameterChange the Handler
signature so that it's just receiving a single object with everything in it. It may look something like this:
interface Context<P extends Params = Params> {
// raw request
request: Request;
// the ServerResponse
response: ServerResponse;
// request extras
params: P;
path: string;
origin: string;
hostname: string;
search: string;
query: URLSearchParams;
// the FetchEvent information
extend: FetchEvent['waitUntil']; // name TBD
waitUntil: FetchEvent['waitUntil']; // name TBD
passThroughOnException: FetchEvent['passThroughOnException']; // name TBD
}
type Handler<
P extends Params = Params,
C extends Context = Context,
> = (context: C<P>) => Promise<Response>;
Now, if you voted for something that involved a Context
object, should worktop auto-create a ServerResponse
for you?
All Context
-based answers assume that ServerRequest
is gone, shifting its properties either to the req
directly or to the Context
object. In this world, Worktop can continue providing a context.response
(name TBD) for you, or you can create it yourself as part of your Router.prepare
hook.
The purists among you may have despised the wannabe-Nodejs all this time, so this would be the opportunity to explicitly opt into ServerResponse
only when needed ... something like this:
import { Router } from 'worktop';
import { ServerResponse } from 'worktop/response';
import type { Context } from 'worktop';
interface MyContext extends Context {
response: ServerResponse;
}
const API = new Router<MyContext>();
API.prepare = function (req, context) {
// assumes no changes to ServerResponse API
context.response = new ServerResponse(req.method);
}
// or
API.prepare = function (context) {
// assumes no changes to ServerResponse API
context.response = new ServerResponse(context.request.method);
}
I know this a lot (too much) to read and sift through. If you managed to go through it β or even some of it βΒ thank you so much. I really really appreciate the feedback.
As titled. Do I miss something?
I did a brief pass over the repo and couldn't find where this is exposed
https://developers.cloudflare.com/workers/learning/fetch-event-lifecycle#passthroughonexception
For example, this code does not working.
const API = new Router();
API.prepare = compose(
async (req, res) => {
// do nothing
// return Promise<void>
}
);
API.add("GET", "/hello", async (req, res) => {
// not called...
});
This snippet from #29 does not work too.
API.prepare = compose(
// Apply CORS globally
CORS.preflight,
// Validate Auth if "/admin/*" route
async (req, res) => {
if (req.pathname.startsWith('/admin/') {
// assume this is Promise<Response | void>
return Auth.validate(req, res);
}
}
);
It seems that call
function inside of compose
returns Response
at last composed function causes this behavior.
Is this intended?
Lots of candy is in the pipe π
API.prepare = compose(
CORS.preflight({
origin: '*',
headers: ['Cache-Control', 'Content-Type'],
methods: ['POST'],
credentials: true,
}),
(request, response) => {
CORS.headers(response, {
origin: request.headers.get('Origin') ?? '*',
});
},
);
This reacts to the origin header on non-OPTIONS requests but it doesn't do it for OPTIONS requests. Any ideas?
According to this page, it looks Cloudflare is replacing addEventHandler("fetch", event => { ... })
for a module syntax that exports async fetch(request, env) {
? To work with durable objects it looks like that env
is needed, and thus it looks like new module syntax would have to be used in worktop instead of the listen
that's currently happening?
https://github.com/lukeed/worktop/blob/master/src/request.ts#L4
$.request = request;
I intend on using this to do a passthrough handler:
export const logOnly: Handler = (request, _) => {
log(await request.body.json())
return await fetch(request.request);
};
As an aside, it also seems like you can't proxy websocket requests with worktop
but you can with default workers. Exposing event.request
would solve this as well.
https://community.cloudflare.com/t/websocket-pass-through-crashes-worker-script/78482/6
I've been going back on forth on this for a while... Basically, it's a debate between:
worktop
CLI that builds a Worker for youI suppose it's possible to do both, but the array of plugins would be second-class in that scenario, especially since it's (probably) unlikely that each integration could offer the same level of output refinement.
PROs
It'd be nice to have a ready-to-go build system for you, allowing (most) users to avoid the webpack that wrangler includes. This would solve low-hanging issues (eg #62, #73, #81). There's already very little reason to use webpack anyway, but really, this is the only unmatched benefit to offering a build-in CLI solution. In other words, even with a worktop/webpack
plugin, there's still the "exports"
and ESM resolving issue that I (as the plugin) can't fix without risking other resolutions breaking.
That said, just to reiterate, any esbuild/swc/Rollup configuration works out of the box. Webpack is purposefully counter-compliant here. π
Either as a worktop
CLI or as a series of plugins, I'm going to want to do AST transformations. For long before worktop's first public release, I've had a PoC that compiles away the worktop Router
in production. Even though the Router is already fast and lightweight, this removes all doubt and produces the most optimized form of the equivalent routing logic.
If I ship a CLI, it's easier to defend including an entire @swc/core
installation. It also means that worktop could lean into other compile-time optimizations/rewrites. Going the plugins route, I'd have to sheepishly include all of @swc/core
anyway, which is kinda like embedding a bundler within your existing bundler, or rewrite all the same AST operations using the target system's offerings ββ i've explored this for another project... it's not fun.
CONs
worktop build
sorta implies the existence/inevitably of a worktop dev
commandminiflare
/equivalent, but the expectation will be there and creates far more scope.Current Stance
Right now, I'm thinking that if I do this, it'd all take place under another package. That would mean that your package.json
looks something like this:
{
"scripts": {
"build": "worktop build"
},
"dependencies": {
"worktop": "latest"
},
"devDependencies": {
"worktop-builder": "latest"
}
}
This way, you only buy into all of the build-time optimizations if you decide to actually use/install worktop-builder
(name tbd). If you don't install and use the toolkit, then you use worktop
as you/we already do today.
Two quick polls to (anonymously) weigh in on the points raised here. Thank you~! π
Plugins vs CLI
Single vs Separate Packages
Fail with No query string was present
in the browser but works with CURL
https://gitlab.fastgraph.de?query={__typename}&extensions={%22persistedQuery%22:{%22version%22:1,%22sha256Hash%22:%22ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38%22}}
CURL
curl --request GET \
--url 'https://gitlab.fastgraph.de?query=%7B__typename%7D&extensions=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%22ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38%22%7D%7D' \
--header 'Accept-Encoding: br' \
--header 'Content-Type: application/json' \
--data '{"query":""}'
Source: https://github.com/StarpTech/FastGraph/blob/main/src/routes/apq.ts
API.add("POST", "/post", handler);
If the user tries a POST request on /invalid
, API.onerror
returns a null error. Is there a way to check that it's an invalid route? Bonus points if it doesn't trigger onerror at all and fails silently.
it would be nice if you made a middleware, against csrf attacks.
Hey there,
first thanks for the great library!
We're using https://github.com/robertcepa/toucan-js to log to sentry. Sadly we need the FetchEvent or at least the request + waitUntil to use it. Did you plan on expose these or add a Logger plugin?
Also could it be instantiated as a composed Handler? In a vanilla worker you would instantiate it before you go for a route to keep the code dry.
Thanks
Alex
Initially I wanted to disallow this entirely (avoid Express nesting nightmare), and while I still mostly lean in that direction, there are valid use cases where the DX is better to have a "subrouter" than to split routing/behavior logic across two locations. (Thanks @evanderkoogh for raising this!)
Suppose you wanted to require Authentication
header validation for all /admin/*
routes:
Before
AKA current
// src/index.js
import * as CORS from 'worktop/cors';
import * as Cache from 'worktop/cache';
import { Router, compose } from 'worktop';
import * as Projects from './routes/projects';
import * as Auth from './utils/auth';
const API = new Router;
API.prepare = compose(
// Apply CORS globally
CORS.preflight,
// Validate Auth if "/admin/*" route
async (req, res) => {
if (req.pathname.startsWith('/admin/') {
// assume this is Promise<Response | void>
return Auth.validate(req, res);
}
}
);
// The actual administrative actions
// > Authentication & Authorization handled above
API.add('GET', '/admin/projects', Projects.list);
API.add('POST', '/admin/projects', Projects.create);
Cache.listen(API.run);
After
// src/routes/admin.js
import { Router } from 'worktop';
import * as Auth from '../utils/auth';
import * as Projects from './projects';
// NOTE: Exported!
export const Admin = new Router;
// All routes in this Router must
// have valid Authentication header
Admin.prepare = Auth.validate;
API.add('GET', '/projects', Projects.list);
API.add('POST', '/projects', Projects.create);
// src/index.js
import { Router } from 'worktop';
import * as CORS from 'worktop/cors';
import * as Cache from 'worktop/cache';
import { Admin } from './routes/admin';
const API = new Router;
// Apply CORS globally
API.prepare = CORS.preflight;
// NEW β direct all "/admin/*" routes to Admin router
API.attach('/admin', Admin);
Cache.listen(API.run);
The important thing to note here is that the API.attach
method (in the last snippet) is the new proposed method. I'm also considering "mount" as the method name... or "direct" lol. I'm purposefully avoiding .all()
and .use()
here, since they're loaded terms β more on that below.
Relatedly, this means that all /admin/*
routes must be handled by the Admin
router. In other words, doing something like this will fail/never hit the foo
or the bar
handlers.
// I WILL NEVER RUN
API.add('GET', '/admin/foo', foo);
// attach subrouter
API.attach('/admin', Admin);
// NEITHER WILL I
API.add('GET', '/admin/bar', bar);
This may seem counterintuitive for Node.js users (especially Express users) since they may be used to the stacking-router model. In worktop, every route points to a single handler β handler/middleware functions can be compose
d into a final handler, but it's still a single handler. This new method name needs to double-down on/imply this distinction. Calling this all()
or use()
would be cause confusion since, coming from Express land, those imply that you're attaching items that expect to work alongside / in conjunction with other handlers. This isn't the case here β it's a complete detour.
Open to feedback & suggestions!
Projects created with wrangler and worktop v0.7.0 and greater fail to run on macOS (Apple M1 and Intel). Details and steps to reproduce follow.
The problem appears to be related to:
In an empty directory:
npm init --yes
wrangler init
npm install [email protected]
wrangler dev
At this point the Cloudflare Workers development environment starts up and serves your API at 127.0.0.1:8787. Everything works as expected.
In the same directory:
npm install [email protected]
(or 0.7.1-0.7.3)wrangler dev
Wrangler fails with the following:
β ~ wrangler dev
π ./node_modules/worktop/router/index.mjs 80:36-37
Can't import the named export 'parse' from non EcmaScript module (only default export is available)
at HarmonyImportSpecifierDependency._getErrors (/Users/rob/Library/Caches/.wrangler/wranglerjs-1.19.0/node_modules/webpack/lib/dependencies/HarmonyImportSpecifierDependency.js:88:6)
at HarmonyImportSpecifierDependency.getErrors (/Users/rob/Library/Caches/.wrangler/wranglerjs-1.19.0/node_modules/webpack/lib/dependencies/HarmonyImportSpecifierDependency.js:68:16)
at Compilation.reportDependencyErrorsAndWarnings (/Users/rob/Library/Caches/.wrangler/wranglerjs-1.19.0/node_modules/webpack/lib/Compilation.js:1463:22)
at /Users/rob/Library/Caches/.wrangler/wranglerjs-1.19.0/node_modules/webpack/lib/Compilation.js:1258:10
at AsyncSeriesHook.eval [as callAsync] (eval at create (/Users/rob/Library/Caches/.wrangler/wranglerjs-1.19.0/node_modules/tapable/lib/HookCodeFactory.js:33:10), <anonymous>:24:1)
at AsyncSeriesHook.lazyCompileHook (/Users/rob/Library/Caches/.wrangler/wranglerjs-1.19.0/node_modules/tapable/lib/Hook.js:154:20)
at Compilation.finish (/Users/rob/Library/Caches/.wrangler/wranglerjs-1.19.0/node_modules/webpack/lib/Compilation.js:1253:28)
at /Users/rob/Library/Caches/.wrangler/wranglerjs-1.19.0/node_modules/webpack/lib/Compiler.js:672:17
at eval (eval at create (/Users/rob/Library/Caches/.wrangler/wranglerjs-1.19.0/node_modules/tapable/lib/HookCodeFactory.js:33:10), <anonymous>:11:1)
at /Users/rob/Library/Caches/.wrangler/wranglerjs-1.19.0/node_modules/webpack/lib/Compilation.js:1185:12
at /Users/rob/Library/Caches/.wrangler/wranglerjs-1.19.0/node_modules/webpack/lib/Compilation.js:1097:9
at processTicksAndRejections (internal/process/task_queues.js:77:11)
@ ./index.js
Error: webpack returned an error. You may be able to resolve this issue by running npm install.
Versions where issue observed:
Second machine (I don't have access to get complete version info):
As of #11, there is now a worktop/crypto
module that includes the following helpers:
digest(algo, message)
SHA1(message)
SHA256(message)
SHA384(message)
SHA512(message)
This ticket exists to collect suggestions for additional helpers that should be added to the module, if any.
So far, I think these would be good additions, if for no other reason than type safety:
declare function importkey(secret: string, algo: ALGO, scopes = ['sign', 'verify']): Promise<CryptoKey>;
declare function verify(secret: string, algo: ALGO, message: string): Promise<ArrayBuffer>;
declare function sign(secret: string, algo: ALGO, message: string): Promise<ArrayBuffer>;
Additionally, I have a PBKDF2 implementation that I can extract from existing application(s) and generalize it:
declare function PBKDF2(input: string, salt: string, iterations: number, keylen: number, digest: string): Promise<ArrayBuffer>;
What else should be here? π
Lastly, WRT importkey
, verify
, and sign
specifically β my applications' implementations only made use of a "raw" imported key:
// example
crypto.subtle.importKey('raw', ....);
Is/Was this application-specific? Or is this "the norm" for a Workers environment?
My hesitation is that these utilities will be too reliant on my importKey
assumption/default and be incorrect for a larger audience.
Thanks!
Very common for password comparison e.g https://github.com/crypto-browserify/timing-safe-equal
import {decode} from 'worktop/base64'
// TypeError: Illegal invocation
decode(match[1])
// works
atob(match[1])
Version: 0.4.2
I see that you have three tickets for modules you intend to provide within Worktop:
I'm a fan of the Worktop project already and am wondering if you've considered the Fastify Decorators and Plugins approach to those kinds of modules:
Rather than implementing a list of modules, create structured entry-points for extending Worktop. Then if a module like CORS is required for a particular use-case, you bring in and register or decorate that module. (Like fastify-cors)
I have a rough fork of Worktop where I've implemented a simple decorator method on the router (see the example below). If you're accepting proposals and contributions, I'm happy to clean it up into a PR for your consideration!
Great work!
import { Router } from 'worktop'
import { listen } from 'worktop/cache'
const API = new Router()
// Decorate an object for this API instance
API.decorate('processObject', process)
// Decorate a function for this API instance
API.decorate('setSeveralHeaders', function (res, [header, value] = []) {
res.setHeader('foo','bar')
res.setHeader('biz','baz')
res.setHeader('wing','bird')
if (header && value) {
res.setHeader(header, value)
}
})
API.add('GET', '/greet/:name', (req, res) => {
// Retrieve the decorated object
const { title } = API.processObject
// Invoke the decorated function
API.setSeveralHeaders(res, ['process-type', title])
res.end(`Hello, ${req.params.name}!`)
})
listen(API.run)
While working on implementing CORS for an API the following example was useful, but buried in a closed PR. This should be surfaced in the docs and examples.
Version 0.7.0 and 0.7.1
wrangler dev
and wrangler publish
Result in:
./node_modules/worktop/router/index.mjs 80:36-37
Can't import the named export 'parse' from non EcmaScript module (only default export is available)
at HarmonyImportSpecifierDependency._getErrors (C:\Users\billy\AppData\Local\.wrangler\wranglerjs-1.19.0\node_modules\webpack\lib\dependencies\HarmonyImportSpecifierDependency.js:88:6)
at HarmonyImportSpecifierDependency.getErrors (C:\Users\billy\AppData\Local\.wrangler\wranglerjs-1.19.0\node_modules\webpack\lib\dependencies\HarmonyImportSpecifierDependency.js:68:16)
at Compilation.reportDependencyErrorsAndWarnings (C:\Users\billy\AppData\Local\.wrangler\wranglerjs-1.19.0\node_modules\webpack\lib\Compilation.js:1463:22)
at C:\Users\billy\AppData\Local\.wrangler\wranglerjs-1.19.0\node_modules\webpack\lib\Compilation.js:1258:10
at AsyncSeriesHook.eval [as callAsync] (eval at create (C:\Users\billy\AppData\Local\.wrangler\wranglerjs-1.19.0\node_modules\tapable\lib\HookCodeFactory.js:33:10), <anonymous>:24:1)
at AsyncSeriesHook.lazyCompileHook (C:\Users\billy\AppData\Local\.wrangler\wranglerjs-1.19.0\node_modules\tapable\lib\Hook.js:154:20)
at Compilation.finish (C:\Users\billy\AppData\Local\.wrangler\wranglerjs-1.19.0\node_modules\webpack\lib\Compilation.js:1253:28)
at C:\Users\billy\AppData\Local\.wrangler\wranglerjs-1.19.0\node_modules\webpack\lib\Compiler.js:672:17
at eval (eval at create (C:\Users\billy\AppData\Local\.wrangler\wranglerjs-1.19.0\node_modules\tapable\lib\HookCodeFactory.js:33:10), <anonymous>:11:1)
at C:\Users\billy\AppData\Local\.wrangler\wranglerjs-1.19.0\node_modules\webpack\lib\Compilation.js:1185:12
at C:\Users\billy\AppData\Local\.wrangler\wranglerjs-1.19.0\node_modules\webpack\lib\Compilation.js:1097:9
at processTicksAndRejections (internal/process/task_queues.js:75:11)
@ ./index.js
Error: webpack returned an error. You may be able to resolve this issue by running npm install.
Hey!
I'm running into:
Uncaught (in promise)
TypeError: Illegal invocation
at worker.js:1:5254
at f (worker.js:1:3637)
at Object.run (worker.js:1:4646)
at worker.js:1:4769
Uncaught (in response)
TypeError: Illegal invocation
When trying to use req.body.text()
and req.body.json()
in a cloudflare worker.
I've wrapped the invocation in a try/catch and the error is empty.
With Worktop
import { Router } from 'worktop'
const API = new Router()
API.add('POST', '/webhook', async function (req, res) {
const body = await req.body.text()
console.log(body)
})
addEventListener('fetch', API.listen)
// TypeError: Illegal invocation
Without Worktop
async function handleRequest (request) {
const body = await request.text()
console.log(body)
}
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
// ok!
Hi,
as stated in https://github.com/StarpTech/GraphCDN/blob/main/src/index.ts I wrap all my handler with the Cache API. Currently, there is only one GET route. Questions:
cache-control: public, max-age: 60, stale-if-error=60
directive and my worker throws or returns 500
, the handler doesn't respond with a stale result. According to https://developers.cloudflare.com/workers/runtime-apis/cache#headers all directives are supported.Hi, from a consumer perspective the module isn't ready for typescript. Typescript has no import support for .mjs
. This results in an error. The workaround described in #27 has no support for sourcemaps and therefore debugger is not supported. It also adds additional overhead to the setup. This isn't absolutely necessary.
I tried conditional exports and it works flawlessly with minimal overhead https://github.com/lukeed/worktop/compare/master...StarpTech:commonjs_support_conditional?expand=1. It would be very welcome to support it until a better solution can be provided.
Being able to modify the onerror
function of the Router
class would work well.
// Adapted from https://developers.cloudflare.com/workers/examples/debugging-logs
API.onerror = (request, response, status, error) => {
request.extend(postLog(error.toString()))
const stack = JSON.stringify(error.stack) || error
const res = new Response(stack, response)
res.headers.set("X-Debug-stack", stack)
res.headers.set("X-Debug-err", error)
return res;
}
Hi. It would be cool if worktop can support WebSocket in the future. It's especially powerful when paired with Durable Object, as explained here.
I have been using https://github.com/uNetworking/uWebSockets.js. and quite like their API as I can define both http and ws routes.
app
.ws(`/room/:id`, wsRouteConfig)
.post('/project/create', projectCreateRoute)
.any(`/*`, (res, _req) => {
res.writeStatus(`404 Not Found`).end()
})
.listen(3001, () => {})
I'm considering switching to Cloudflare since I won't have to think about scaling infrastructure, which is enticing.
Hi, any reason why the TTL options aren't exposed?
https://developers.cloudflare.com/workers/runtime-apis/kv#expiring-keys
Version 0.6.3
router.add('GET', '/', (request, response) => {
console.log(request.headers)
response.send(200, request.headers)
})
Returns
{}
https://developers.cloudflare.com/workers/examples/cache-post-request
worktop
already sets Cache-Control
to private, no-cache
so a good solution may be to apply POST caching when this header gets modified.
Main use case for this is to cache GraphQL queries
Right now, the uid
export inside worktop/utils
is using a hexadecimal alphabet (abcdef1234567890
), which it shared with the uuid
export.
I think it should move to a full alphanumeric dictionary instead: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-_
Of course, this allows for more combinations with shorter output lengths. For example, a to produce a 6-char string:
// hexadecimal (current)
6 ** 16; // 2.8211099e+12
// alphanumeric (proposed)
6 ** 64; // 6.3340287e+49
Creating a 1280-length character string using the current hexadecimal format has 5.1922969e+49
combinations, which is still fewer than a 6-length alphanumeric (full) alphabet.
Because the dictionary/options are changing, this would be a breaking change.
Or worktop/kvs
or worktop/database
Depends if/how Durable Objects get worked in or if they should be their own separate module.
NOTE@SELF: Port this over from App2
Is there an intuitive way to do this that I'm not seeing?
Some things I've tried:
request.params
(pretty bad for obvious reasons)response.setHeader
and response.getHeader
(not as bad but still pretty tedious)Need to filter through my existing applications and extract the things that don't look app-specific.
This will be a (mostly) random hodgepodge of utilities. If there's enough methods that can fall under a loosely-defined umbrella, then I'll move those to their own submodule. An example of this is that I think I may have enough to justify a worktop/crypto
submodule... we'll see.
After several weeks working with Cloudflare workers, I just realized how the KV storage behaves from an application perspective. As mentioned in the documentation reads and write are eventually consistent.
Due to the eventually consistent nature of Workers KV, concurrent writes from different edge locations can end up up overwriting one another. Itβs a common pattern to write data via Wrangler or the API but read the data from within a worker, avoiding this issue by issuing all writes from the same location.
Note that get may return stale values -- if a given key has recently been read in a given location, changes to the key made in other locations may take up to 60 seconds to be visible. See How KV works for more on this topic.
Ref: https://developers.cloudflare.com/workers/runtime-apis/kv#reading-key-value-pairs
This means we can't write based on a previous read state. In the TODO example, we will lose data when multiple users create TODO's in the time range of the synchronization between the edges.
I opened this issue to communicate that circumstance a bit deeper. Durable Objects will solve it but I'm surprised that they can only hold 32KiB π
The current worktop/crypto
module has keyload
and keygen
methods, but they're really not all that helpful. I end up dropping into crypto.subtle.importKey
(and others) directly quite often, which is a good signal that something should be done here.
Additionally, the native TS definitions for importKey
et all are pretty useless... With custom methods, I should be able to accurately offer strict overloads so that you can only have/define valid combinations. For example, for importKey
has its own restrictions and generateKey
has different input requirements based on format. Both of these can offer much better types - and possibly a tiny abstraction.
The idea is to allow the same worker to service multiple domains ("zones"). Through Worker Routes, there's no reason why you can't/shouldn't be able to do this in a real application.
And with SSL for SaaS, this may be way more common.
hostname
to request dataApp.host('example.com').add('GET', '/foo', handler)
APIAs of #23, the compose
export now exists. However, it only has basic req.params
value forwarding.
This issue remains open so that compose
can (*should) guard against Handler
s that are loaded before their required properties have been satisfied.
For example:
import { compose } from 'worktop';
import type { Handler } from 'worktop';
import type { ServerRequest } from 'worktop/request';
import type { User, App } from 'lib/models';
type UserRequest = ServerRequest & { user: User };
type AppRequest = ServerRequest & { app: App };
// Assume it reads `Authorization` header, and loads `req.user` or bails
// ~> only "needs" a bare `ServerRequest`, provides a `UserRequest`
// HINT: Read the type argument left-to-right for Request mutation
declare const toUser: Handler<ServerRequest, UserRequest>;
// Assume it reads loads the relevant `App` record
// ~> only "needs" a bare `ServerRequest`, provides a `AppRequest`
declare const toApp: Handler<ServerRequest, AppRequest>;
// Validates that `req.user` is the owner of `req.app` record
// ~> NEEDS `req.user` & `req.app` to exist, provides no other changes
declare const isOwner: Handler<UserRequest & AppRequest>;
// @ts-expect-error :: no `AppRequest` involved
compose(toUser, isOwner);
// @ts-expect-error :: no `UserRequest` involved
compose(toApp, isOwner);
// @ts-expect-error :: isOwner before `UserRequest` defined
compose(toApp, isOwner, toUser);
// @ts-expect-error :: isOwner before `AppRequest` defined
compose(toUser, isOwner, toApp);
// TYPE SAFE π
compose(toUser, toApp, isOwner);
Of all the examples above, only the last line is (*should be) considered "type safe" because both req.user
and req.app
are defined before isOwner
runs, which is a function that needs both of those properties to exist.
Hi, great framework!
I think it can be very useful to add a util function to generate unique lexicographic keys.
Based on https://developers.cloudflare.com/workers/runtime-apis/kv#ordering the KV keys are sorted accordingly.
Example implementation: https://github.com/ulid/javascript.
Version: 0.4.2
tsconfig.json
{
"compilerOptions": {
"outDir": "build",
"module": "commonjs",
"target": "esnext",
"lib": ["esnext", "webworker"],
"alwaysStrict": true,
"strict": true,
"preserveConstEnums": true,
"moduleResolution": "node",
"sourceMap": true,
"esModuleInterop": true,
"types": ["@cloudflare/workers-types"]
},
"include": [
"./src/**/*.ts"
],
"exclude": ["node_modules/", "dist/"]
}
> tsc --noEmit
node_modules/worktop/request/index.d.ts:5:3 - error TS2717: Subsequent property declarations must have the same type. Property 'cf' must be of type 'IncomingRequestCfProperties', but here has type 'IncomingCloudflareProperties'.
5 cf: IncomingCloudflareProperties;
~~
node_modules/@cloudflare/workers-types/index.d.ts:313:3
313 cf: IncomingRequestCfProperties;
~~
'cf' was also declared here.
Hey Luke,
Many thanks for working on worktop and cfw πππ.
Is there any chance for worktop + cfw template?
Currently, i fork from kv-todos for new projects.
Have a great weekend, btw!
This is labeled as maybe because 2/3 of the exports listed as just aliases of built-in methods. However, this may still be useful because, personally, I rarely remember the atob
-vs-btoa
direction on the first try π
Exports:
encode
(alias for btoa
)decode
(alias for atob
)toURL
β URL-safe Base64 valueAnother possibility is that only toURL
is added to worktop/utils
as toBase64URL
& then users are left to remember btoa
-vs-atob
on their own π¬
I think I personally prefer a base64
module, even if it's somewhat trivial. Thoughts?
Hello, I can't import worktop
in a test because it loads .mjs
files and I have no idea how to load .mjs
with typescript files. I use node-tap
as test runner. Maybe it's possible to fix it with https://nodejs.org/api/packages.html#packages_conditional_exports
.../node_modules/worktop/request/index.mjs:18
src/routes/foo.test.ts 2> export {
SyntaxError: Unexpected token 'export'
package.json
"scripts": {
"test": "tap --ts",
},
Hi @lukeed. First of all, thank you for worktop! Using this feels like a dream compared to what I was putting together before when working with Cloudflare workers.
I was following along this Fauna tutorial and ran into the following issue when running wrangler dev
(and wrangler publish
). I noticed that this error only triggers for v0.7.0 and that using v.0.6.3 does not trigger the error and the project runs fine.
Do you know what the problem might be? I'm not using TypeScript and my node version is v15.2.1. This was a pretty minimal project as described in the tutorial but please let me know if you want me to create a repo that can recreate the error.
π ./node_modules/worktop/router/index.mjs 80:36-37
Can't import the named export 'parse' from non EcmaScript module (only default export is available)
at HarmonyImportSpecifierDependency._getErrors (~/Library/Caches/.wrangler/wranglerjs-1.17.0/node_modules/webpack/lib/dependencies/HarmonyImportSpecifierDependency.js:90:6)
...
Edit: Just want to add that I have tried this in another project without faunadb
and worktop
is the only dependency.
This code:
import type {Handler} from 'worktop';
import {connect} from 'worktop/ws';
export const handler: Handler = async (request) => {
const error = connect(request);
if (error) return error;
};
produces the following Eslint errors:
β 5:17 Unsafe call of an any typed value. @typescript-eslint/no-unsafe-call
β 6:14 Unsafe return of an any typed value. @typescript-eslint/no-unsafe-return
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.