Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: proxy handler .byName #28

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .changeset/small-zebras-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
"hono-do": minor
---

Add `byName` handler for making DO proxy easier.

## Without proxy

```ts
app.all("/counter/*", (c) => {
const id = c.env.COUNTER.idFromName("Counter");
const obj = c.env.COUNTER.get(id);
return obj.fetch(c.req.raw);
});
```

## With proxy

```ts
app.use("/counter/*", Counter.byName("COUNTER", "Counter"));
```

`.byName` is a handler to proxy the request to the DO object with the given name.
The first argument is the name of the Binding namespace, the second is the name of the DO object.

### Dynamic name

```ts
export const Room = generateHonoObject("/room/:roomId", (app) => {
app.get("/", (c) => c.text(`Room ${c.req.param("roomId")}`));
})

app.use("/room/*", Room.byName("ROOM", (c) => c.req.param("roomId")));
```

Using callback as the second argument, the name of the DO object can be dynamic.
You can separate objects for each request path.
9 changes: 9 additions & 0 deletions examples/proxy-counter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Hono DO Counter (with byName Proxy)

This example is a counter app with byName proxy.
[Counter example](../counter)'s rewrite with byName proxy.

```
pnpm install
pnpm dev
```
21 changes: 21 additions & 0 deletions examples/proxy-counter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "hono-do-example-proxy-counter",
"private": true,
"version": "0.0.4",
"scripts": {
"lint": "eslint --fix --ext .ts,.tsx src",
"lint:check": "eslint --ext .ts,.tsx src",
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx}\"",
"dev": "wrangler dev src/index.ts --port 3000",
"deploy": "wrangler deploy --minify src/index.ts"
},
"dependencies": {
"hono": "^3.6.0",
"hono-do": "workspace:*"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20230821.0",
"wrangler": "^3.7.0"
}
}
20 changes: 20 additions & 0 deletions examples/proxy-counter/src/counter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { generateHonoObject } from "hono-do";

export const Counter = generateHonoObject("/counter", async (app, state) => {
const { storage } = state;
let value = (await storage.get<number>("value")) ?? 0;

app.post("/increment", (c) => {
storage.put("value", value++);
return c.text(value.toString());
});

app.post("/decrement", (c) => {
storage.put("value", value--);
return c.text(value.toString());
});

app.get("/", (c) => {
return c.text(value.toString());
});
});
15 changes: 15 additions & 0 deletions examples/proxy-counter/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Hono } from "hono";

import { Counter } from "./counter";
import { Template } from "./template";

const app = new Hono();

app.use("/counter/*", Counter.byName("COUNTER", "counter"));

app.get("/", (c) => {
return c.html(Template);
});

export default app;
export * from "./counter";
30 changes: 30 additions & 0 deletions examples/proxy-counter/src/template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { html } from "hono/html";

export const Template = html`
<html>
<body>
<h1>Counter</h1>
<p>Current value: <span id="value"></span></p>
<button id="increment">Increment</button>
<button id="decrement">Decrement</button>
<script>
const value = document.getElementById("value");
const increment = document.getElementById("increment");
const decrement = document.getElementById("decrement");
const updateValue = async () => {
const res = await fetch("/counter");
value.innerText = await res.text();
};
increment.addEventListener("click", async () => {
await fetch("/counter/increment", { method: "POST" });
await updateValue();
});
decrement.addEventListener("click", async () => {
await fetch("/counter/decrement", { method: "POST" });
await updateValue();
});
updateValue();
</script>
</body>
</html>
`.trim();
17 changes: 17 additions & 0 deletions examples/proxy-counter/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"lib": [
"esnext"
],
"types": [
"@cloudflare/workers-types"
],
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
},
}
9 changes: 9 additions & 0 deletions examples/proxy-counter/wrangler.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
name = "proxy-counter"
compatibility_date = "2023-01-01"

[durable_objects]
bindings = [{ name = "COUNTER", class_name = "Counter" }]

[[migrations]]
tag = "v1"
new_classes = ["Counter"]
2 changes: 2 additions & 0 deletions packages/hono-do/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ export class HonoDOError extends Error {
export const Errors = {
handlerAlreadySet: (handlerName: string) =>
new HonoDOError(`Handler ${handlerName} already set`),
namespaceNotSet: (namespace: string) =>
new HonoDOError(`Namespace ${namespace} not set`),
};
11 changes: 11 additions & 0 deletions packages/hono-do/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,17 @@ export function generateHonoObject<
return honoObject;
};

honoObject.byName = function (namespace, name) {
return async (c) => {
const target = c.env[namespace] as DurableObjectNamespace | undefined;
if (target == null) throw Errors.namespaceNotSet(namespace);
const _name = typeof name === "function" ? await name(c) : name;
const id = target.idFromName(_name);
const obj = target.get(id);
return obj.fetch(c.req.raw);
};
};

return honoObject;
}

Expand Down
11 changes: 10 additions & 1 deletion packages/hono-do/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Env, Hono, Schema } from "hono";
import { Context, Env, Hono, MiddlewareHandler, Schema } from "hono";

import { MergeArray } from "./utils";

Expand Down Expand Up @@ -30,6 +30,15 @@ export interface HonoObject<
webSocketError: (
handler: WebSocketErrorHandler,
) => HonoObject<E, S, BasePath>;
byName: <
T extends string,
E extends {
Bindings: { [K in T]: DurableObjectNamespace };
},
>(
namespace: T,
name: string | ((c: Context<E, BasePath>) => string | Promise<string>),
) => MiddlewareHandler;
}

export type AlarmHandler = (
Expand Down
16 changes: 16 additions & 0 deletions packages/hono-do/tests/fixtures/by-name-proxy-dynamic/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Hono } from "hono";

import { generateHonoObject } from "../../../src";

const app = new Hono();

export const Room = generateHonoObject("/room/:roomId", async (app) => {
app.get("/", (c) => c.text(c.req.param("roomId")));
});

app.use(
"/room/*",
Room.byName("ROOM", (c) => c.req.param("roomId")),
);

export default app;
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ name = "hono-do-test"
compatibility_date = "2023-01-01"

[durable_objects]
bindings = [{ name = "DELAY_INIT", class_name = "DelayInit" }]
bindings = [{ name = "ROOM", class_name = "Room" }]

[[migrations]]
tag = "v1"
new_classes = ["DelayInit"]
new_classes = ["Room"]
13 changes: 13 additions & 0 deletions packages/hono-do/tests/fixtures/by-name-proxy-simple/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Hono } from "hono";

import { generateHonoObject } from "../../../src";

const app = new Hono();

export const Simple = generateHonoObject("/simple", async (app) => {
app.get("/", (c) => c.text("Hello, Hono DO!"));
});

app.use("*", Simple.byName("SIMPLE", "simple"));

export default app;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
name = "hono-do-test"
compatibility_date = "2023-01-01"

[durable_objects]
bindings = [{ name = "SIMPLE", class_name = "Simple" }]

[[migrations]]
tag = "v1"
new_classes = ["Simple"]
24 changes: 0 additions & 24 deletions packages/hono-do/tests/fixtures/delay-init/index.ts

This file was deleted.

77 changes: 54 additions & 23 deletions packages/hono-do/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,29 +96,6 @@ describe("Worker", () => {
});
});

describe("DelayInit", () => {
let worker: UnstableDevWorker;

beforeEach(async () => {
worker = await unstable_dev(
join(__dirname, "fixtures/delay-init/index.ts"),
{
experimental: { disableExperimentalWarning: true },
},
);
});

afterEach(async () => {
await worker.stop();
});

it("should wait for initialization", async () => {
const resp = await worker.fetch("/delay-init/count");
expect(resp.status).toBe(200);
expect(await resp.text()).toBe("1");
});
});

describe("Alarm", () => {
let worker: UnstableDevWorker;

Expand Down Expand Up @@ -174,4 +151,58 @@ describe("Worker", () => {
}
});
});

describe("ByName Proxy", () => {
describe("Simple", () => {
let worker: UnstableDevWorker;

beforeEach(async () => {
worker = await unstable_dev(
join(__dirname, "fixtures/by-name-proxy-simple/index.ts"),
{
experimental: { disableExperimentalWarning: true },
},
);
});

afterEach(async () => {
await worker.stop();
});

it("should work with byName", async () => {
const resp = await worker.fetch("/simple");
expect(resp.status).toBe(200);
expect(await resp.text()).toBe("Hello, Hono DO!");
});
});

describe("Dynamic", () => {
let worker: UnstableDevWorker;

beforeEach(async () => {
worker = await unstable_dev(
join(__dirname, "fixtures/by-name-proxy-dynamic/index.ts"),
{
experimental: { disableExperimentalWarning: true },
},
);
});

afterEach(async () => {
await worker.stop();
});

it("should work with byName", async () => {
const roomId = Array.from({ length: 10 }).map(() =>
Math.random().toString(36).slice(2),
);

for (const id of roomId) {
const resp = await worker.fetch(`/room/${id}`);
expect(resp.status).toBe(200);
expect(await resp.text()).toBe(id);
}
});
});
});
});
Loading