Skip to content

Commit

Permalink
feat: pipe universal middlewares (#19)
Browse files Browse the repository at this point in the history
* feat: compose universal middlewares

* chore: simplify types

* chore: export compose

* refactor: rename compose to pipe

* refactor: rename compose to pipe

* refactor: rename compose to pipe
  • Loading branch information
magne4000 authored Aug 22, 2024
1 parent aa5cf5e commit c13e8f5
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 2 deletions.
2 changes: 1 addition & 1 deletion examples/tool/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,4 @@
"@universal-middleware/hono": "^0",
"@universal-middleware/webroute": "^0"
}
}
}
4 changes: 3 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"license": "MIT",
"scripts": {
"build": "rimraf dist && tsup",
"test": "vitest --typecheck run",
"prepack": "pnpm build",
"test:typecheck": "tsc -p tsconfig.json --noEmit",
"release": "LANG=en_US release-me patch",
Expand All @@ -27,7 +28,8 @@
"@types/node": "^20.14.10",
"rimraf": "^6.0.0",
"tsup": "^8.2.4",
"typescript": "^5.5.4"
"typescript": "^5.5.4",
"vitest": "^2.0.5"
},
"sideEffects": false
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export type * from "./types.js";
export * from "./runtime.js";
export * from "./adapter.js";
export * from "./utils.js";
export * from "./pipe.js";
70 changes: 70 additions & 0 deletions packages/core/src/pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Awaitable, UniversalHandler, UniversalMiddleware } from "./types";

type Out<T> = T extends UniversalMiddleware<any, infer C> ? C : never;
type In<T> =
T extends UniversalHandler<infer C>
? C
: T extends UniversalMiddleware<infer C, any>
? C
: never;
type First<T extends any[]> = T extends [infer X, ...any[]] ? X : never;
type Last<T extends any[]> = T extends [...any[], infer X] ? X : never;

type ComposeReturnType<T extends UniversalMiddleware<any, any>[]> =
Last<T> extends UniversalHandler<any>
? UniversalHandler<In<First<T>>>
: UniversalMiddleware<In<First<T>>, In<Last<T>>>;

type Pipe<F extends UniversalMiddleware<any, any>[]> = F extends []
? F
: F extends [UniversalMiddleware<any, any>]
? F
: F extends [
UniversalMiddleware<infer A, infer B>,
UniversalMiddleware<any, infer D>,
]
? [UniversalMiddleware<A, B>, UniversalMiddleware<B, D>]
: F extends [
...infer X extends UniversalMiddleware<any, any>[],
infer Y extends UniversalMiddleware<any, any>,
UniversalMiddleware<any, infer D1>,
]
? [...Pipe<[...X, Y]>, UniversalMiddleware<Out<Y>, D1>]
: never;

export function pipe<F extends UniversalMiddleware<any, any>[]>(
...a: Pipe<F>
): ComposeReturnType<F> {
const middlewares = a as UniversalMiddleware[];
const handler = a.pop() as UniversalHandler;

return async (request, context, runtime) => {
const pending: ((response: Response) => Awaitable<Response>)[] = [];

for (const m of middlewares) {
const response = await m(request, context, runtime);

if (typeof response === "function") {
pending.push(response);
} else if (response !== null && typeof response === "object") {
if (response instanceof Response) {
return response;
}
// Update context
context = response as any;
}
}

let response = await handler(request, context, runtime);

for (const m of pending) {
const r = await m(response);
if (r) {
response = r;
}
}

return response;
};
}
67 changes: 67 additions & 0 deletions packages/core/test/pipe.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { expectTypeOf, test } from "vitest";
import type {
UniversalHandler,
UniversalMiddleware,
} from "@universal-middleware/core";
import { pipe } from "../src/pipe";

type M1 = UniversalMiddleware<{ a: 1 }, { a: 1; b: 2 }>;
type M2 = UniversalMiddleware<{ a: 1; b: 2 }, { a: 1; b: 2; c: 3 }>;
type M3 = UniversalMiddleware<{ a: 1; b: 2; c: 3 }, { a: 1; b: 2; c: 3 }>;
type H1 = UniversalHandler<{ a: 1; b: 2; c: 3 }>;

test("pipe", () => {
const m1: M1 = {} as any;
const m2: M2 = {} as any;
const m3: M3 = {} as any;
const h1: H1 = {} as any;

expectTypeOf(pipe(m3, h1)).toEqualTypeOf<
UniversalHandler<{ a: 1; b: 2; c: 3 }>
>(h1);
expectTypeOf(
pipe(
m1,
// @ts-expect-error
h1,
),
).toEqualTypeOf<UniversalHandler<{ a: 1 }>>();
expectTypeOf(pipe(m1, m2, m3, h1)).toEqualTypeOf<
UniversalHandler<{ a: 1 }>
>();
expectTypeOf(
pipe(
m1,
m1,
// @ts-expect-error
h1,
),
).toEqualTypeOf<UniversalHandler<{ a: 1 }>>();
expectTypeOf(
pipe(
m1,
// @ts-expect-error
m3,
h1,
),
).toEqualTypeOf<UniversalHandler<{ a: 1 }>>();

expectTypeOf(pipe(m1, m2, m3)).toEqualTypeOf<
UniversalMiddleware<{ a: 1 }, { a: 1; b: 2; c: 3 }>
>();

expectTypeOf(
pipe((_: Request) => new Response(null)),
).toEqualTypeOf<UniversalHandler>();
expectTypeOf(
pipe(
() => {
return {
a: "1",
};
},
(a: Request, c: { a: string }) => new Response(c.a),
),
).toEqualTypeOf<UniversalHandler>();
});
96 changes: 96 additions & 0 deletions packages/core/test/pipe.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, expect, test } from "vitest";
import { pipe } from "../src/pipe";
import type { RuntimeAdapter, UniversalMiddleware } from "../src/index";

describe("pipe", () => {
const request = new Request("http://localhost");
const context: Universal.Context = {};
const runtime: RuntimeAdapter = {
runtime: "other",
adapter: "other",
};

test("handler", async () => {
const handler = pipe(() => new Response("OK"));
const response = handler(request, context, runtime);
await expect(response).resolves.toBeInstanceOf(Response);
});

test("context middleware |> handler", async () => {
const handler = pipe(
() => ({ a: 1 }),
(_: Request, ctx: { a: number }) => new Response(String(ctx.a)),
);
const response = handler(request, context, runtime);
await expect(response).resolves.toBeInstanceOf(Response);

const body = await (await response).text();
expect(body).toBe("1");
});

test("context middleware |> empty middlware |> handler", async () => {
const handler = pipe(
() => ({ a: 1 }),
(async () => {}) as UniversalMiddleware<{ a: number }, { a: number }>,
(_: Request, ctx: { a: number }) => new Response(String(ctx.a)),
);
const response = handler(request, context, runtime);
await expect(response).resolves.toBeInstanceOf(Response);

const body = await (await response).text();
expect(body).toBe("1");
});

test("context middleware |> context middleware |> handler", async () => {
const handler = pipe(
() => ({ a: 1 }),
async (_: Request, ctx: { a: number }) => {
return {
...ctx,
b: 2,
};
},
(_: Request, ctx: { a: number; b: number }) =>
new Response(String(ctx.a + ctx.b)),
);
const response = handler(request, context, runtime);
await expect(response).resolves.toBeInstanceOf(Response);

const body = await (await response).text();
expect(body).toBe("3");
});

test("context middleware |> response |> handler", async () => {
const handler = pipe(
() => ({ a: 1 }),
async (_: Request) => {
return new Response("STOPPED");
},
(_: Request) => new Response(null),
);
const response = handler(request, context, runtime);
await expect(response).resolves.toBeInstanceOf(Response);

const body = await (await response).text();
expect(body).toBe("STOPPED");
});

test("context middleware |> response handler |> handler", async () => {
const handler = pipe(
() => ({ a: 1 }),
(_: Request) => {
return async (response: Response) => {
const body = await (await response).text();
return new Response(body + " World!");
};
},
(_: Request) => new Response("Hello"),
);
const response = handler(request, context, runtime);
await expect(response).resolves.toBeInstanceOf(Response);

const body = await (await response).text();
expect(body).toBe("Hello World!");
});
});
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit c13e8f5

Please sign in to comment.