diff --git a/.changeset/small-zebras-leave.md b/.changeset/small-zebras-leave.md new file mode 100644 index 00000000..d92ecddc --- /dev/null +++ b/.changeset/small-zebras-leave.md @@ -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. diff --git a/examples/proxy-counter/README.md b/examples/proxy-counter/README.md new file mode 100644 index 00000000..086736e3 --- /dev/null +++ b/examples/proxy-counter/README.md @@ -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 +``` diff --git a/examples/proxy-counter/package.json b/examples/proxy-counter/package.json new file mode 100644 index 00000000..0c68d990 --- /dev/null +++ b/examples/proxy-counter/package.json @@ -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" + } +} diff --git a/examples/proxy-counter/src/counter.ts b/examples/proxy-counter/src/counter.ts new file mode 100644 index 00000000..ece49083 --- /dev/null +++ b/examples/proxy-counter/src/counter.ts @@ -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("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()); + }); +}); diff --git a/examples/proxy-counter/src/index.ts b/examples/proxy-counter/src/index.ts new file mode 100644 index 00000000..815719de --- /dev/null +++ b/examples/proxy-counter/src/index.ts @@ -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"; diff --git a/examples/proxy-counter/src/template.ts b/examples/proxy-counter/src/template.ts new file mode 100644 index 00000000..35636fd2 --- /dev/null +++ b/examples/proxy-counter/src/template.ts @@ -0,0 +1,30 @@ +import { html } from "hono/html"; + +export const Template = html` + + +

Counter

+

Current value:

+ + + + + +`.trim(); diff --git a/examples/proxy-counter/tsconfig.json b/examples/proxy-counter/tsconfig.json new file mode 100644 index 00000000..9cd84898 --- /dev/null +++ b/examples/proxy-counter/tsconfig.json @@ -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" + }, +} \ No newline at end of file diff --git a/examples/proxy-counter/wrangler.toml b/examples/proxy-counter/wrangler.toml new file mode 100644 index 00000000..247bcffe --- /dev/null +++ b/examples/proxy-counter/wrangler.toml @@ -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"] diff --git a/packages/hono-do/src/error.ts b/packages/hono-do/src/error.ts index a16c900d..4e4e4c12 100644 --- a/packages/hono-do/src/error.ts +++ b/packages/hono-do/src/error.ts @@ -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`), }; diff --git a/packages/hono-do/src/index.ts b/packages/hono-do/src/index.ts index 48e0c5bf..d36706f9 100644 --- a/packages/hono-do/src/index.ts +++ b/packages/hono-do/src/index.ts @@ -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; } diff --git a/packages/hono-do/src/types.ts b/packages/hono-do/src/types.ts index 98a98327..f2a02e52 100644 --- a/packages/hono-do/src/types.ts +++ b/packages/hono-do/src/types.ts @@ -1,4 +1,4 @@ -import { Env, Hono, Schema } from "hono"; +import { Context, Env, Hono, MiddlewareHandler, Schema } from "hono"; import { MergeArray } from "./utils"; @@ -30,6 +30,15 @@ export interface HonoObject< webSocketError: ( handler: WebSocketErrorHandler, ) => HonoObject; + byName: < + T extends string, + E extends { + Bindings: { [K in T]: DurableObjectNamespace }; + }, + >( + namespace: T, + name: string | ((c: Context) => string | Promise), + ) => MiddlewareHandler; } export type AlarmHandler = ( diff --git a/packages/hono-do/tests/fixtures/by-name-proxy-dynamic/index.ts b/packages/hono-do/tests/fixtures/by-name-proxy-dynamic/index.ts new file mode 100644 index 00000000..9258b7b2 --- /dev/null +++ b/packages/hono-do/tests/fixtures/by-name-proxy-dynamic/index.ts @@ -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; diff --git a/packages/hono-do/tests/fixtures/delay-init/wrangler.toml b/packages/hono-do/tests/fixtures/by-name-proxy-dynamic/wrangler.toml similarity index 52% rename from packages/hono-do/tests/fixtures/delay-init/wrangler.toml rename to packages/hono-do/tests/fixtures/by-name-proxy-dynamic/wrangler.toml index c91d287b..2cc9cbfa 100644 --- a/packages/hono-do/tests/fixtures/delay-init/wrangler.toml +++ b/packages/hono-do/tests/fixtures/by-name-proxy-dynamic/wrangler.toml @@ -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"] diff --git a/packages/hono-do/tests/fixtures/by-name-proxy-simple/index.ts b/packages/hono-do/tests/fixtures/by-name-proxy-simple/index.ts new file mode 100644 index 00000000..23068787 --- /dev/null +++ b/packages/hono-do/tests/fixtures/by-name-proxy-simple/index.ts @@ -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; diff --git a/packages/hono-do/tests/fixtures/by-name-proxy-simple/wrangler.toml b/packages/hono-do/tests/fixtures/by-name-proxy-simple/wrangler.toml new file mode 100644 index 00000000..52df923c --- /dev/null +++ b/packages/hono-do/tests/fixtures/by-name-proxy-simple/wrangler.toml @@ -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"] diff --git a/packages/hono-do/tests/fixtures/delay-init/index.ts b/packages/hono-do/tests/fixtures/delay-init/index.ts deleted file mode 100644 index 45395f9e..00000000 --- a/packages/hono-do/tests/fixtures/delay-init/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Hono } from "hono"; - -import { generateHonoObject } from "../../../src"; - -const app = new Hono<{ - Bindings: { - DELAY_INIT: DurableObjectNamespace; - }; -}>(); - -app.all("/delay-init/*", (c) => { - const id = c.env.DELAY_INIT.idFromName("delay-init"); - const obj = c.env.DELAY_INIT.get(id); - return obj.fetch(c.req.raw); -}); - -export const DelayInit = generateHonoObject("/delay-init", async (app) => { - let count = 0; - app.get("/count", (c) => c.text(String(count))); - await new Promise((resolve) => setTimeout(resolve, 1000)); - count = 1; -}); - -export default app; diff --git a/packages/hono-do/tests/index.test.ts b/packages/hono-do/tests/index.test.ts index 08ef45cf..fca2bf3b 100644 --- a/packages/hono-do/tests/index.test.ts +++ b/packages/hono-do/tests/index.test.ts @@ -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; @@ -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); + } + }); + }); + }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f3bee4e9..71d5235c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,6 +131,22 @@ importers: specifier: ^3.7.0 version: 3.8.0 + examples/proxy-counter: + dependencies: + hono: + specifier: ^3.6.0 + version: 3.6.0 + hono-do: + specifier: workspace:* + version: link:../../packages/hono-do + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20230821.0 + version: 4.20230904.0 + wrangler: + specifier: ^3.7.0 + version: 3.8.0 + packages/hono-do: dependencies: hono: