+
+Routes and middleware are added to a `Router` instance with `.use`, `.addRoute` or `.get/post/put/delete`.
+
+The router is then used with your web server of choice, e.g. `Deno.serve`!
```js
-import * as Peko from "https://deno.land/x/peko/mod.ts";
-// import from ".../peko/lib/Server.ts" for featherweight mode
+import * as Peko from "https://deno.land/x/peko/mod.ts";
+
+const router = new Peko.Router();
+
+router.use(Peko.logger(console.log));
-const server = new Peko.Server();
+router.get("/shorthand-route", () => new Response("Hello world!"));
-server.use(Peko.logger(console.log));
+router.post("/shorthand-route-ext", async (ctx, next) => { await next(); console.log(ctx.request.headers); }, (req) => new Response(req.body));
-server.get("/hello", () => new Response("Hello world!"));
+router.addRoute({
+ path: "/object-route",
+ middleware: async (ctx, next) => { await next(); console.log(ctx.request.headers); }, // can also be array of middleware
+ handler: () => new Response("Hello world!")
+})
-server.listen(7777, () => console.log("Peko server started - let's go!"));
+router.addRoutes([ /* array of route objects */ ])
+
+Deno.serve((req) => router.requestHandler(req))
```
+
Types
+
+### [**Router**](https://deno.land/x/peko/mod.ts?s=Router)
+The main class of Peko, provides `requestHandler` method to generate `Response` from `Request` via configured routes and middleware.
+
+### [**Route**](https://deno.land/x/peko/mod.ts?s=Route)
+Objects with `path`, `method`, `middleware`, and `handler` properties. Requests are matched to a regex generated from the given path. Dynamic parameters are supported in the `/users/:userid` syntax.
+
+### [**RequestContext**](https://deno.land/x/peko/mod.ts?s=RequestContext)
+An object containing `url`, `params` and `state` properties that is provided to all middleware and handler functions associated to a router or matched route.
+
+### [**Middleware**](https://deno.land/x/peko/mod.ts?s=Middleware)
+Functions that receives a RequestContext and a next fcn. Should update `ctx.state`, perform side-effects or return a response.
+
+### [**Handler**](https://deno.land/x/peko/mod.ts?s=Handler)
+The final request handling function on a `Route`. Must generate and return a response using the provided request context.
+
+
Request handling
+
+Each route must have a handler function that generates a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response). Upon receiving a request the `Server` will construct a [RequestContext](https://deno.land/x/peko/server.ts?s=RequestContext) and cascade it through any global middleware, then route middleware and finally the route handler. Global and route middleware are invoked in the order they are added. If a response is returned by any middleware along the chain no subsequent middleware/handler will run.
+
+Peko comes with a library of utilities, middleware and handlers for common route use-cases, such as:
+- server-side-rendering
+- opening WebSockets/server-sent events
+- JWT signing/verifying & authentication
+- logging
+- caching
+
+See `handlers`, `mmiddleware` or `utils` for source, or dive into `examples` for demo implementations.
+
+The second argument to any middleware is the `next` fcn. This returns a promise that resolves to the first response returned by any subsequent middleware/handler. This is useful for error-handling as well as post-response operations such as editing headers or logging. See the below snippet or `middleware/logger.ts` for examples.
+
+If no matching route is found for a request an empty 404 response is sent. If an error occurs in handling a request an empty 500 response is sent. Both of these behaviours can be overwritten with the following middleware:
+
+```js
+router.use(async (_, next) => {
+ const response = await next();
+ if (!response) return new Response("Would you look at that? Nothing's here!", { status: 404 });
+});
+```
+
+```js
+router.use(async (_, next) => {
+ try {
+ await next();
+ } catch(e) {
+ console.log(e);
+ return new Response("Oh no! An error occured :(", { status: 500 });
+ }
+});
+```
+
+
Response caching
+
+In stateless computing, memory should only be used for source code and disposable cache data. Response caching ensures that we only store data that can be regenerated or refetched. Peko provides a `ResponseCache` utility for this with configurable item lifetime. The `cacher` middleware wraps it and provides drop in handler memoization and response caching for your routes.
+
+```js
+const cache = new Peko.ResponseCache({ lifetime: 5000 });
+
+router.addRoute("/do-stuff", Peko.cacher(cache), () => new Response(Date.now()));
+```
+
+And that's it! Check out the API docs for deeper info. Otherwise happy coding 🤓
+
App showcase
PR to add your project 🙌
@@ -122,5 +199,3 @@ The modern JavaScript edge rocks because the client-server gap practically disap
This is made possible by engines such as Deno that are built to the [ECMAScript](https://tc39.es/) specification (support for URL module imports is the secret sauce). UI libraries like [Preact](https://github.com/preactjs/preact) combined with [htm](https://github.com/developit/htm) offer lightning fast client-side hydration with a browser-friendly markup syntax. Deno also has native TypeScript support, a rich runtime API and loads of community tools for your back-end needs.
If you are interested in contributing please submit a PR or get in contact ^^
-
-Read `overview.md` for a more detailed guide on using Peko.
diff --git a/examples/auth/app.ts b/examples/auth/app.ts
index 83a07df5..bef699b8 100644
--- a/examples/auth/app.ts
+++ b/examples/auth/app.ts
@@ -1,6 +1,7 @@
-import * as Peko from "https://deno.land/x/peko/mod.ts"
+import * as Peko from "../../mod.ts" // "https://deno.land/x/peko/mod.ts"
-const server = new Peko.Server()
+const html = String
+const router = new Peko.Router()
const crypto = new Peko.Crypto("SUPER_SECRET_KEY_123") // <-- replace from env
const user = { // <-- replace with db / auth provider query
username: "test-user",
@@ -13,8 +14,8 @@ const validateUser = async (username: string, password: string) => {
&& await crypto.hash(password) === user.password
}
-server.use(Peko.logger(console.log))
-server.post("/login", async (ctx) => {
+router.use(Peko.logger(console.log))
+router.post("/login", async (ctx) => {
const { username, password } = await ctx.request.json()
if (!await validateUser(username, password)) {
@@ -37,14 +38,13 @@ server.post("/login", async (ctx) => {
})
})
-server.get(
+router.get(
"/verify",
Peko.authenticator(crypto),
() => new Response("You are authenticated!")
)
-const html = String
-server.get("/", Peko.ssrHandler(() => html`
+router.get("/asdf", Peko.ssrHandler(() => html`
Peko auth example
@@ -132,4 +132,4 @@ server.get("/", Peko.ssrHandler(() => html`
`))
-server.listen()
\ No newline at end of file
+Deno.serve((req) => router.requestHandler(req))
\ No newline at end of file
diff --git a/examples/preact/index.ts b/examples/preact/index.ts
index 155a852a..cae8eab9 100644
--- a/examples/preact/index.ts
+++ b/examples/preact/index.ts
@@ -1,20 +1,20 @@
-import { Server, logger } from "https://deno.land/x/peko/mod.ts"
+import { Router, logger } from "../../mod.ts" //"https://deno.land/x/peko/mod.ts"
import pages from "./routes/pages.ts"
import assets from "./routes/assets.ts"
import APIs from "./routes/APIs.ts"
// initialize server
-const server = new Server()
-server.use(logger(console.log))
+const router = new Router()
+router.use(logger(console.log))
// SSR'ed app page routes
-server.addRoutes(pages)
+router.addRoutes(pages)
// Static assets
-server.addRoutes(assets)
+router.addRoutes(assets)
// Custom API functions
-server.addRoutes(APIs)
+router.addRoutes(APIs)
-// Start Peko server :^)
-server.listen()
\ No newline at end of file
+// Start Deno server with Peko router :^)
+Deno.serve((req) => router.requestHandler(req))
\ No newline at end of file
diff --git a/examples/preact/routes/APIs.ts b/examples/preact/routes/APIs.ts
index 2dc5c2de..cadbeef3 100644
--- a/examples/preact/routes/APIs.ts
+++ b/examples/preact/routes/APIs.ts
@@ -2,7 +2,7 @@ import {
RequestContext,
Route,
sseHandler
-} from "https://deno.land/x/peko/mod.ts"
+} from "../../../mod.ts"
const demoEventTarget = new EventTarget()
setInterval(() => {
diff --git a/examples/preact/routes/assets.ts b/examples/preact/routes/assets.ts
index 5d3a18bd..1d61ed12 100644
--- a/examples/preact/routes/assets.ts
+++ b/examples/preact/routes/assets.ts
@@ -3,7 +3,7 @@ import {
staticHandler,
cacher,
ResponseCache
-} from "https://deno.land/x/peko/mod.ts"
+} from "../../../mod.ts"
import { recursiveReaddir } from "https://deno.land/x/recursive_readdir@v2.0.0/mod.ts"
import { fromFileUrl } from "https://deno.land/std@0.174.0/path/mod.ts"
diff --git a/examples/preact/routes/pages.ts b/examples/preact/routes/pages.ts
index 3e08f1af..6eb17363 100644
--- a/examples/preact/routes/pages.ts
+++ b/examples/preact/routes/pages.ts
@@ -3,7 +3,7 @@ import {
ssrHandler,
cacher,
ResponseCache
-} from "https://deno.land/x/peko/mod.ts"
+} from "../../../mod.ts"
import { renderToString } from "https://npm.reversehttp.com/preact,preact/hooks,htm/preact,preact-render-to-string"
diff --git a/examples/preact/src/components/Layout.js b/examples/preact/src/components/Layout.js
index 3ff0baad..426fcd6f 100644
--- a/examples/preact/src/components/Layout.js
+++ b/examples/preact/src/components/Layout.js
@@ -5,7 +5,8 @@ const Layout = ({ navColor, navLink, children }) => {
@@ -26,7 +27,7 @@ const Layout = ({ navColor, navLink, children }) => {
HomeAbout
-
diff --git a/lib/utils/Router.ts b/lib/Router.ts
similarity index 58%
rename from lib/utils/Router.ts
rename to lib/Router.ts
index 4a1ef292..ab2ee691 100644
--- a/lib/utils/Router.ts
+++ b/lib/Router.ts
@@ -1,12 +1,27 @@
-import { Middleware, Handler, Route } from "../types.ts"
-import { Cascade } from "./Cascade.ts"
+import { Cascade } from "./utils/Cascade.ts"
+import { Middleware, Handler, Route } from "./types.ts"
+
+export class RequestContext {
+ url: URL
+ state: Record
+ params: Record = {}
+
+ constructor(
+ public router: Router,
+ public request: Request,
+ state?: Record
+ ) {
+ this.url = new URL(request.url)
+ this.state = state ? state : {}
+ }
+}
export class _Route implements Route {
path: `/${string}`
params: Record = {}
regexPath: RegExp
method?: "GET" | "POST" | "PUT" | "DELETE"
- middleware?: Middleware[] | Middleware
+ middleware: Middleware[] = []
handler: Handler
constructor(routeObj: Route) {
@@ -18,8 +33,8 @@ export class _Route implements Route {
if (str[0] === ":") this.params[str.slice(1)] = i
});
this.regexPath = this.params
- ? new RegExp(this.path.replaceAll(/(?<=\/):(.)*?(?=\/|$)/g, "(.)*"))
- : new RegExp(this.path)
+ ? new RegExp(`^${this.path.replaceAll(/(?<=\/):(.)*?(?=\/|$)/g, "(.)*")}\/?$`)
+ : new RegExp(`^${this.path}\/?$`)
this.method = routeObj.method || "GET"
this.handler = Cascade.promisify(routeObj.handler!) as Handler
@@ -31,20 +46,35 @@ export class _Route implements Route {
}
export class Router {
- constructor(public routes: _Route[] = []) {}
-
- static applyDefaults(routeObj: Partial): Route {
- if (!routeObj.path) throw new Error("Route is missing path")
- if (!routeObj.handler) throw new Error("Route is missing handler")
+ constructor(
+ public routes: _Route[] = [],
+ public middleware: Middleware[] = []
+ ) {}
- routeObj.method = routeObj.method || "GET"
- routeObj.handler = Cascade.promisify(routeObj.handler!) as Handler
- routeObj.middleware = [routeObj.middleware]
- .flat()
- .filter(Boolean)
- .map((mware) => Cascade.promisify(mware!))
+ /**
+ * Generate Response by running route middleware/handler with Cascade.
+ * @param request: Request
+ * @returns Promise
+ */
+ async requestHandler(request: Request): Promise {
+ const ctx = new RequestContext(this, request)
+ const res = await new Cascade(ctx, this.middleware).run()
+ return res
+ ? res
+ : new Response("", { status: 404 })
+ }
- return routeObj as Route
+ /**
+ * Add global middleware or another router's middleware
+ * @param middleware: Middleware[] | Middleware | Router
+ * @returns number - server.middleware.length
+ */
+ use(middleware: Middleware | Middleware[]) {
+ if (Array.isArray(middleware)) {
+ middleware.forEach(mware => this.use(mware))
+ return middleware.length
+ }
+ return this.middleware.push(Cascade.promisify(middleware))
}
/**
@@ -69,13 +99,29 @@ export class Router {
? { path: arg1, handler: arg2 as Handler }
: { path: arg1, ...arg2 as Partial }
: { path: arg1, middleware: arg2 as Middleware | Middleware[], handler: arg3 }
+
+ const fullRoute = new _Route(routeObj as Route)
- if (this.routes.find(existing => existing.path === routeObj.path)) {
+ if (this.routes.find(existing => existing.regexPath.toString() === fullRoute.regexPath.toString())) {
throw new Error(`Route with path ${routeObj.path} already exists!`)
}
-
- const fullRoute = new _Route(routeObj as Route)
+
this.routes.push(fullRoute)
+ this.middleware.push(function RouteMiddleware(ctx) {
+ if (
+ fullRoute.regexPath.test(ctx.url.pathname) &&
+ fullRoute.method === ctx.request.method
+ ) {
+ if (fullRoute?.params) {
+ const pathBits = ctx.url.pathname.split("/")
+ for (const param in fullRoute.params) {
+ ctx.params[param] = pathBits[fullRoute.params[param]]
+ }
+ }
+
+ return new Cascade(ctx, [...fullRoute.middleware, fullRoute.handler]).run()
+ }
+ })
return fullRoute
}
@@ -126,34 +172,34 @@ export class Router {
/**
* Add Routes
* @param routes: Route[] - middleware can be Middlewares or Middleware
- * @returns number - routes.length
+ * @returns Route[] - added routes
*/
- addRoutes(routes: Route[]): number {
- routes.forEach(route => this.addRoute(route))
- return this.routes.length
+ addRoutes(routes: Route[]): Route[] {
+ return routes.map(route => this.addRoute(route))
}
/**
* Remove Route from Peko server
* @param route: Route["path"] of route to remove
- * @returns
+ * @returns Route - removed route
*/
- removeRoute(route: Route["path"]): number {
+ removeRoute(route: Route["path"]): Route | undefined {
const routeToRemove = this.routes.find(r => r.path === route)
if (routeToRemove) {
this.routes.splice(this.routes.indexOf(routeToRemove), 1)
}
- return this.routes.length
+ return routeToRemove
}
/**
* Remove Routes
* @param routes: Route["path"] of routes to remove
- * @returns
+ * @returns Array - removed routes
*/
- removeRoutes(routes: Route["path"][]): number {
- routes.forEach(route => this.removeRoute(route))
- return this.routes.length
+ removeRoutes(routes: Route["path"][]): Array {
+ return routes.map(route => this.removeRoute(route))
}
-}
\ No newline at end of file
+}
+
+export default Router
\ No newline at end of file
diff --git a/lib/Server.ts b/lib/Server.ts
deleted file mode 100644
index 3cfb523a..00000000
--- a/lib/Server.ts
+++ /dev/null
@@ -1,130 +0,0 @@
-import { Server as stdServer } from "https://deno.land/std@0.174.0/http/server.ts"
-import { Router, _Route } from "./utils/Router.ts"
-import { Cascade, PromiseMiddleware } from "./utils/Cascade.ts"
-import { Middleware } from "./types.ts"
-
-export class RequestContext {
- url: URL
- _route: _Route | undefined
- state: Record
- params: Record = {}
-
- constructor(
- public server: Server,
- public request: Request,
- state?: Record
- ) {
- this.url = new URL(request.url)
- this.state = state ? state : {}
- }
-
- get route () {
- return this._route
- }
-
- set route (r: _Route | undefined) {
- this._route = r;
- if (r?.params) {
- const pathBits = this.url.pathname.split("/")
- for (const param in r.params) {
- this.params[param] = pathBits[r.params[param]]
- }
- }
- }
-}
-
-export class Server extends Router {
- stdServer: stdServer | undefined
- port = 7777
- hostname = "127.0.0.1"
- middleware: PromiseMiddleware[] = []
- routers: Router[] = []
-
- public get allRoutes(): _Route[] {
- return [ this, ...this.routers].map(router => router.routes).flat()
- }
-
- constructor(config?: {
- port?: number,
- hostname?: string,
- }) {
- super()
- if (!config) return
- const { port, hostname } = config
- if (port) this.port = port
- if (hostname) this.hostname = hostname
- }
-
- /**
- * Add global middleware or another router
- * @param middleware: Middleware[] | Middleware | Router
- * @returns number - server.middleware.length
- */
- use(middleware: Middleware | Middleware[] | Router) {
- if (middleware instanceof Router) {
- return this.routers.push(middleware)
- }
-
- if (Array.isArray(middleware)) {
- middleware.forEach(mware => this.use(mware))
- return middleware.length
- }
- return this.middleware.push(Cascade.promisify(middleware))
- }
-
- /**
- * Start listening to HTTP requests.
- * @param port: number
- * @param onListen: onListen callback function
- * @param onError: error handler
- */
- async listen(
- port?: number,
- onListen?: (server: stdServer) => void,
- onError?: (error: unknown) => Response | Promise
- ): Promise {
- if (port) this.port = port
-
- this.stdServer = new stdServer({
- port: this.port,
- hostname: this.hostname,
- handler: (request: Request) => this.requestHandler.call(this, request),
- onError
- })
-
- if (onListen) {
- onListen(this.stdServer)
- } else {
- console.log(`Peko server started on port ${this.port} with routes:`)
- this.allRoutes.forEach((route, i) => console.log(`${route.method} ${route.path} ${i===this.routes.length-1 ? "\n" : ""}`))
- }
-
- return await this.stdServer.listenAndServe()
- }
-
- /**
- * Generate Response by running route middleware/handler with Cascade.
- * @param request: Request
- * @returns Promise
- */
- async requestHandler(request: Request): Promise {
- const ctx: RequestContext = new RequestContext(this, request)
-
- ctx.route = this.allRoutes.find(route =>
- route.regexPath.test(ctx.url.pathname) &&
- route.method === request.method
- )
-
- return await new Cascade(ctx).start()
- }
-
- /**
- * Stop listening to HTTP requests.
- * @param port: number
- * @param onListen: onListen callback function
- * @param onError: onListen callback function
- */
- close(): void {
- if (this.stdServer) this.stdServer.close()
- }
-}
diff --git a/lib/handlers/sse.ts b/lib/handlers/sse.ts
index 88e9e3a4..8fb578d1 100644
--- a/lib/handlers/sse.ts
+++ b/lib/handlers/sse.ts
@@ -1,5 +1,5 @@
-import { Handler, HandlerOptions } from "../types.ts"
import { mergeHeaders } from "../utils/helpers.ts"
+import { Handler, HandlerOptions } from "../types.ts"
const encoder = new TextEncoder()
diff --git a/lib/handlers/ssr.ts b/lib/handlers/ssr.ts
index 2ec7a40f..9664a75e 100644
--- a/lib/handlers/ssr.ts
+++ b/lib/handlers/ssr.ts
@@ -1,7 +1,7 @@
-import { RequestContext } from "../Server.ts"
-import { Handler, HandlerOptions } from "../types.ts"
+import { RequestContext } from "../Router.ts"
import { Crypto } from "../utils/Crypto.ts"
import { mergeHeaders } from "../utils/helpers.ts"
+import { Handler, HandlerOptions } from "../types.ts"
export type Render = (ctx: RequestContext) => BodyInit | Promise
export interface ssrHandlerOptions extends HandlerOptions {
diff --git a/lib/handlers/static.ts b/lib/handlers/static.ts
index 297b99dd..a0215953 100644
--- a/lib/handlers/static.ts
+++ b/lib/handlers/static.ts
@@ -1,9 +1,9 @@
import { contentType } from "https://deno.land/std@0.174.0/media_types/mod.ts";
import { fromFileUrl } from "https://deno.land/std@0.174.0/path/mod.ts"
-import { RequestContext } from "../Server.ts"
-import { Handler, HandlerOptions } from "../types.ts"
+import { RequestContext } from "../Router.ts"
import { Crypto } from "../utils/Crypto.ts"
import { mergeHeaders } from "../utils/helpers.ts"
+import { Handler, HandlerOptions } from "../types.ts"
const crypto = new Crypto(Array.from({length: 10}, () => Math.floor(Math.random() * 9)).toString())
export interface staticHandlerOptions extends HandlerOptions {
diff --git a/lib/handlers/ws.ts b/lib/handlers/ws.ts
deleted file mode 100644
index 505afaca..00000000
--- a/lib/handlers/ws.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { Handler, HandlerOptions } from "../types.ts"
-
-/**
- * Upgrades requests with "upgrade: websocket" header to WebSocket connection.
- * Streams type "send" CustomEvents from provided EventTarget to client.
- * Dispatches received MessageEvents to provided EventTarget.
- * Routes using this handler should be requested via the WebSocket browser API.
- * @param target: EventTarget
- * @param opts: (optional) HandlerOptions
- * @returns Handler: (ctx: RequestContext) => Promise
- */
-export const wsHandler = (socketCallback: (s: WebSocket) => unknown, opts: HandlerOptions = {}): Handler => (ctx) => {
- const { request } = ctx
- const conn = request.headers.get("connection") || "";
- const upgrade = request.headers.get("upgrade") || "";
- if (!conn.toLowerCase().includes("upgrade") || upgrade.toLowerCase() != "websocket") {
- return new Response("request isn't trying to upgrade to websocket.", { status: 400 });
- }
-
- const { socket, response } = Deno.upgradeWebSocket(request);
- socketCallback(socket)
-
- if (opts.headers) for (const header of opts.headers) {
- response.headers.append(header[0], header[1])
- }
-
- return response
-}
\ No newline at end of file
diff --git a/lib/middleware/authenticator.ts b/lib/middleware/authenticator.ts
index 45e043fb..772412c9 100644
--- a/lib/middleware/authenticator.ts
+++ b/lib/middleware/authenticator.ts
@@ -1,5 +1,5 @@
-import { Middleware } from "../types.ts"
import { Crypto } from "../utils/Crypto.ts"
+import { Middleware } from "../types.ts"
/**
* Auth middleware, uses Crypto utility class to verify JWTs
diff --git a/lib/middleware/cacher.ts b/lib/middleware/cacher.ts
index 035d0831..849868e6 100644
--- a/lib/middleware/cacher.ts
+++ b/lib/middleware/cacher.ts
@@ -1,6 +1,6 @@
-import type { RequestContext } from "../Server.ts";
-import { Middleware } from "../types.ts";
+import type { RequestContext } from "../Router.ts";
import { ResponseCache } from "../utils/ResponseCache.ts";
+import { Middleware } from "../types.ts";
// default key generator
const defaultKeygen = (ctx: RequestContext) => {
diff --git a/lib/types.ts b/lib/types.ts
index ba0e80b8..36f1eb8e 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -1,4 +1,4 @@
-import { RequestContext } from "./Server.ts"
+import { RequestContext } from "./Router.ts"
export interface Route {
path: `/${string}`
@@ -7,7 +7,7 @@ export interface Route {
handler: Handler
}
-export type Result = void | Response
+export type Result = void | Response | undefined
export type Next = () => Promise | Result
export type Middleware = (ctx: RequestContext, next: Next) => Promise | Result
diff --git a/lib/utils/Cascade.ts b/lib/utils/Cascade.ts
index 6444fdb4..cf5d0d5e 100644
--- a/lib/utils/Cascade.ts
+++ b/lib/utils/Cascade.ts
@@ -1,21 +1,16 @@
-import { RequestContext } from "../Server.ts"
+import { RequestContext } from "../Router.ts"
import { Middleware, Result, Next } from "../types.ts"
export type PromiseMiddleware = (ctx: RequestContext, next: Next) => Promise
/**
- * Utility class for running middleware functions as a cascade
+ * Utility class for running middleware functions in a cascade
*/
export class Cascade {
- response: Response | undefined
+ result: Result
called = 0
- toCall: PromiseMiddleware[]
- constructor(public ctx: RequestContext) {
- this.toCall = this.ctx.route
- ? [...this.ctx.server.middleware, ...this.ctx.route.middleware as PromiseMiddleware[], this.ctx.route.handler as PromiseMiddleware]
- : [...this.ctx.server.middleware]
- }
+ constructor(public ctx: RequestContext, public middleware: Middleware[]) {}
static promisify = (fcn: Middleware): PromiseMiddleware => {
return fcn.constructor.name === "AsyncFunction"
@@ -25,24 +20,17 @@ export class Cascade {
})
}
- async run(fcn: PromiseMiddleware): Promise {
- if (!fcn) return this.response
-
- try {
- const response = await fcn(this.ctx, async () => await this.run(this.toCall[++this.called]))
- if (response) this.response = response
- if (!this.response) await this.run(this.toCall[++this.called])
- } catch (error) {
- throw error
+ async run(): Promise {
+ if (this.middleware[this.called]) {
+ try {
+ const res = await this.middleware[this.called++](this.ctx, () => this.run.call(this))
+ if (res) this.result = res
+ else return await this.run()
+ } catch (error) {
+ throw error
+ }
}
-
- return this.response
- }
-
- async start() {
- await this.run(this.toCall[this.called])
- return this.response
- ? this.response
- : new Response("", { status: 404 })
+
+ return this.result
}
}
diff --git a/lib/utils/Profiler.ts b/lib/utils/Profiler.ts
index c181f8d9..29524d86 100644
--- a/lib/utils/Profiler.ts
+++ b/lib/utils/Profiler.ts
@@ -1,4 +1,4 @@
-import { Server } from "../Server.ts"
+import { Router } from "../Router.ts"
import { Route } from "../types.ts"
type ProfileConfig = {
@@ -20,22 +20,22 @@ type ProfileResults = Record<
}
>
-class Profiler {
+export class Profiler {
/**
* Benchmark performance of all server routes one at a time
- * @param server
+ * @param router
* @param config
* @returns results: ProfileResults
*/
- static async run(server: Server, config?: ProfileConfig) {
- const url = (config && config.url) || `http://${server.hostname}:${server.port}`
+ static async run(router: Router, config?: ProfileConfig) {
+ const url = (config && config.url) || `http://localhost:7777`
const count = (config && config.count) || 100
const excludedRoutes = (config && config.excludedRoutes) || []
const mode = (config && config.mode) || "serve"
const results: ProfileResults = {}
- for (const route of server.routes) {
+ for (const route of router.routes) {
results[route.path] = { avgTime: 0, requests: [] }
if (!excludedRoutes.includes(route)) {
@@ -45,7 +45,7 @@ class Profiler {
const start = Date.now()
const response = mode === "serve"
? await fetch(routeUrl)
- : await server.requestHandler(new Request(routeUrl))
+ : await router.requestHandler(new Request(routeUrl))
const end = Date.now()
return results[route.path].requests.push({
diff --git a/mod.ts b/mod.ts
index 0aa295ad..42bb109a 100644
--- a/mod.ts
+++ b/mod.ts
@@ -3,7 +3,7 @@
*/
// Core classes, functions & types
-export * from "./lib/Server.ts"
+export * from "./lib/Router.ts"
export * from "./lib/types.ts"
// Handlers
@@ -17,11 +17,10 @@ export * from "./lib/middleware/cacher.ts"
export * from "./lib/middleware/authenticator.ts"
// Utils
-export * from "./lib/utils/Router.ts"
export * from "./lib/utils/Cascade.ts"
export * from "./lib/utils/ResponseCache.ts"
export * from "./lib/utils/Crypto.ts"
export * from "./lib/utils/helpers.ts"
-import { Server } from "./lib/Server.ts"
-export default Server
\ No newline at end of file
+import { Router } from "./lib/Router.ts"
+export default Router
\ No newline at end of file
diff --git a/overview.md b/overview.md
deleted file mode 100644
index e7901e5b..00000000
--- a/overview.md
+++ /dev/null
@@ -1,115 +0,0 @@
-
Peko library overview
-
-
-
-
Server
-
-The TypeScript `server.ts` module describes a small framework for building HTTP servers on top of the Deno http/server module.
-
-Here are the main components:
-
-- **Server class**: which manages the HTTP server, the routes, and the middleware.
-- **RequestContext class:** holds information about the server, the request, and state to be shared between middleware.
-
-Main types (`types.ts`):
-
-- **Route**: an object with path, method, middleware, and handler properties.
-- **Middleware**: a function that receives a RequestContext and updates state or generates a response.
-- **Handler**: a function that handles requests by receiving a RequestContext and generating a response.
-
-The Server class has several methods for adding and removing routes and middleware, as well as starting the server and handling requests:
-
-- **use(middleware: Middleware | Middleware[] | Router)**: add global middleware or a router.
-- **addRoute(route: Route)**: adds a route to the server.
-- **addRoutes(routes: Route[])**: adds multiple routes to the server.
-- **removeRoute(route: string)**: removes a route from the server.
-- **removeRoutes(routes: string[])**: removes multiple routes from the server.
-- **listen(port?: number, onListen?: callback)**: starts listening to HTTP requests on the specified port.
-- **close()**: stops to HTTP listener process.
-
-```js
-import * as Peko from "https://deno.land/x/peko/mod.ts"; // or "../server.ts" for super featherweight
-
-const server = new Peko.Server();
-
-server.use(Peko.logger(console.log));
-
-server.addRoute("/hello", () => new Response("Hello world!"));
-
-server.listen(7777, () => console.log("Peko server started - let's go!"));
-```
-
-
Routing
-
-Routes can be added to a Server instance directly or a Router instance. Below you can see the different ways routes can be added with `addRoute`.
-
-```js
-import * as Peko from "https://deno.land/x/peko/mod.ts"; // or "https://deno.land/x/peko/server.ts"
-
-const server = new Peko.Server()
-server.addRoute("/hello", () => new Response("Hello world!"))
-server.removeRoute("/hello");
-
-const router = new Peko.Router()
-
-router.addRoute("/shorthand-route", async (ctx, next) => { await next(); console.log(ctx.request.headers); }, () => new Response("Hello world!"));
-
-router.addRoute({
- path: "/object-route",
- middleware: async (ctx, next) => { await next(); console.log(ctx.request.headers); }, // can also be array of middleware
- handler: () => new Response("Hello world!")
-})
-
-router.addRoutes([ /* array of route objects */ ])
-
-server.use(router)
-
-server.listen()
-```
-
-
Request handling
-
-Each route must have a handler function that generates a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response). Upon receiving a request the `Server` will construct a [RequestContext](https://deno.land/x/peko/server.ts?s=RequestContext) and cascade it through any global middleware, then route middleware and finally the route handler. Global and route middleware are invoked in the order they are added. If a response is returned by any middleware along the chain no subsequent middleware/handler will run.
-
-Peko comes with a library of utilities, middleware and handlers for common route use-cases, such as:
-- server-side-rendering
-- opening WebSockets
-- JWT signing/verifying & authentication
-- logging
-- caching
-
-See `handlers`, `mmiddleware` or `utils` for source, or dive into `examples` for demo implementations.
-
-The second argument to any middleware is the `next` fcn. This returns a promise that resolves to the first response returned by any subsequent middleware/handler. This is useful for error-handling as well as post-response operations such as logging. See the below snippet or `middleware/logger.ts` for examples.
-
-If no matching route is found for a request an empty 404 response is sent. If an error occurs in handling a request an empty 500 response is sent. Both of these behaviours can be overwritten with the following middleware:
-
-```js
-server.use(async (_, next) => {
- const response = await next();
- if (!response) return new Response("Would you look at that? Nothing's here!", { status: 404 });
-});
-```
-
-```js
-server.use(async (_, next) => {
- try {
- await next();
- } catch(e) {
- console.log(e);
- return new Response("Oh no! An error occured :(", { status: 500 });
- }
-});
-```
-
-