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: add some DurableObject's custom field handler #21

Merged
merged 11 commits into from
Nov 30, 2023
36 changes: 36 additions & 0 deletions .changeset/three-actors-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
"hono-do": minor
---

Support for three handlers about [Hibernation Websocket API](https://developers.cloudflare.com/durable-objects/learning/websockets/#websocket-hibernation).

- `webSocketMessage` handler
- `webSocketClose` handler
- `webSocketError` handler

You can use these handlers same way as `alarm` handler in Hono DO.

## Usage

### Flat way

```ts
const DO = generateHonoObject("/", () => {});
DO.alarm(async () => {});
DO.webSocketMessage(async () => {});
DO.webSocketClose(async () => {});
DO.webSocketError(async () => {});
```

### Chaining way

```ts
generateHonoObject("/", () => {})
.alarm(async () => {})
.webSocketMessage(async () => {})
.webSocketClose(async () => {})
.webSocketError(async () => {});
```

Take care for registering multiple handlers for same event.
If you register so, you will get an error.
11 changes: 11 additions & 0 deletions packages/hono-do/src/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export class HonoDOError extends Error {
constructor(message: string) {
super(message);
this.name = "HonoDOError";
}
}

export const Errors = {
handlerAlreadySet: (handlerName: string) =>
new HonoDOError(`Handler ${handlerName} already set`),
};
127 changes: 83 additions & 44 deletions packages/hono-do/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { Env, Hono, Schema } from "hono";

export interface HonoObjectVars {}

interface HonoObject<
E extends Env = Env,
S extends Schema = Record<string, never>,
BasePath extends string = "/",
> {
app: Hono<E, S, BasePath>;
state: DurableObjectState;
vars: HonoObjectVars;
}
import { HonoDOError, Errors } from "./error";
import {
AlarmHandler,
WebSocketMessageHandler,
WebSocketCloseHandler,
WebSocketErrorHandler,
HonoObjectHandlers,
HonoObjectVars,
HonoObject,
} from "./types";

export function generateHonoObject<
E extends Env = Env,
Expand All @@ -23,52 +22,92 @@ export function generateHonoObject<
state: DurableObjectState,
vars: HonoObjectVars,
) => void | Promise<void>,
alarm?: (
state: DurableObjectState,
vars: HonoObjectVars,
) => void | Promise<void>,
handlers: HonoObjectHandlers = {},
) {
const honoObject = function (
this: HonoObject<E, S, BasePath>,
state: DurableObjectState,
) {
const app = new Hono<E, S, BasePath>().basePath(basePath);
const _handlers: HonoObjectHandlers = {
...handlers,
};

const app = new Hono<E, S, BasePath>().basePath(basePath);

const honoObject = function (this, state) {
this.app = app;
this.state = state;
this.vars = {};
state.blockConcurrencyWhile(async () => {
await cb(app, state, this.vars);
});
};
} as HonoObject<E, S, BasePath>;

honoObject.prototype.fetch = function (
this: HonoObject<E, S, BasePath>,
request: Request,
...args: Parameters<NonNullable<DurableObject["fetch"]>>
) {
return this.app.fetch(...args);
};

honoObject.prototype.alarm = async function (
this: HonoObject<E, S, BasePath>,
) {
await _handlers.alarm?.(this.state, this.vars);
};

honoObject.prototype.webSocketMessage = function (
this: HonoObject<E, S, BasePath>,
...args: Parameters<NonNullable<DurableObject["webSocketMessage"]>>
) {
_handlers.webSocketMessage?.(...args, this.state, this.vars);
};

honoObject.prototype.webSocketClose = function (
this: HonoObject<E, S, BasePath>,
...args: Parameters<NonNullable<DurableObject["webSocketClose"]>>
) {
_handlers.webSocketClose?.(...args, this.state, this.vars);
};

honoObject.prototype.webSocketError = function (
this: HonoObject<E, S, BasePath>,
...args: Parameters<NonNullable<DurableObject["webSocketError"]>>
) {
return this.app.fetch(request);
_handlers.webSocketError?.(...args, this.state, this.vars);
};

const isCalledMap = new Map<string, boolean>();

honoObject.alarm = function (handler: AlarmHandler) {
const name = "alarm";
if (isCalledMap.get(name)) throw Errors.handlerAlreadySet(name);
_handlers.alarm = handler;
isCalledMap.set(name, true);
return honoObject;
};

honoObject.webSocketMessage = function (handler: WebSocketMessageHandler) {
const name = "webSocketMessage";
if (isCalledMap.get(name)) throw Errors.handlerAlreadySet(name);
_handlers.webSocketMessage = handler;
isCalledMap.set(name, true);
return honoObject;
};

if (alarm !== undefined) {
honoObject.prototype.alarm = async function (
this: HonoObject<E, S, BasePath>,
) {
await alarm(this.state, this.vars);
};
} else {
honoObject.alarm = function (
cb: (
state: DurableObjectState,
vars: HonoObjectVars,
) => void | Promise<void>,
) {
honoObject.prototype.alarm = async function (
this: HonoObject<E, S, BasePath>,
) {
await cb(this.state, this.vars);
};
return honoObject;
};
}
honoObject.webSocketClose = function (handler: WebSocketCloseHandler) {
const name = "webSocketClose";
if (isCalledMap.get(name)) throw Errors.handlerAlreadySet(name);
_handlers.webSocketClose = handler;
isCalledMap.set(name, true);
return honoObject;
};

honoObject.webSocketError = function (handler: WebSocketErrorHandler) {
const name = "webSocketError";
if (isCalledMap.get(name)) throw Errors.handlerAlreadySet(name);
_handlers.webSocketError = handler;
isCalledMap.set(name, true);
return honoObject;
};

return honoObject;
}

export { HonoObjectVars, HonoDOError };
68 changes: 68 additions & 0 deletions packages/hono-do/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Env, Hono, Schema } from "hono";

import { MergeArray } from "./utils";

export interface HonoObjectVars {}

interface HonoObjectState<
E extends Env = Env,
S extends Schema = Record<string, never>,
BasePath extends string = "/",
> {
app: Hono<E, S, BasePath>;
state: DurableObjectState;
vars: HonoObjectVars;
}

export interface HonoObject<
E extends Env = Env,
S extends Schema = Record<string, never>,
BasePath extends string = "/",
> extends HonoObjectState<E, S, BasePath> {
(this: HonoObject<E, S, BasePath>, state: DurableObjectState): void;
alarm: (handler: AlarmHandler) => HonoObject<E, S, BasePath>;
webSocketMessage: (
handler: WebSocketMessageHandler,
) => HonoObject<E, S, BasePath>;
webSocketClose: (
handler: WebSocketCloseHandler,
) => HonoObject<E, S, BasePath>;
webSocketError: (
handler: WebSocketErrorHandler,
) => HonoObject<E, S, BasePath>;
}

export type AlarmHandler = (
...args: MergeArray<
Parameters<NonNullable<DurableObject["alarm"]>>,
[state: DurableObjectState, vars: HonoObjectVars]
>
) => ReturnType<NonNullable<DurableObject["alarm"]>>;

export type WebSocketMessageHandler = (
...args: MergeArray<
Parameters<NonNullable<DurableObject["webSocketMessage"]>>,
[state: DurableObjectState, vars: HonoObjectVars]
>
) => ReturnType<NonNullable<DurableObject["webSocketMessage"]>>;

export type WebSocketCloseHandler = (
...args: MergeArray<
Parameters<NonNullable<DurableObject["webSocketClose"]>>,
[state: DurableObjectState, vars: HonoObjectVars]
>
) => ReturnType<NonNullable<DurableObject["webSocketClose"]>>;

export type WebSocketErrorHandler = (
...args: MergeArray<
Parameters<NonNullable<DurableObject["webSocketError"]>>,
[state: DurableObjectState, vars: HonoObjectVars]
>
) => ReturnType<NonNullable<DurableObject["webSocketError"]>>;

export interface HonoObjectHandlers {
alarm?: AlarmHandler;
webSocketMessage?: WebSocketMessageHandler;
webSocketClose?: WebSocketCloseHandler;
webSocketError?: WebSocketErrorHandler;
}
6 changes: 6 additions & 0 deletions packages/hono-do/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type MergeArray<T extends unknown[], U extends unknown[]> = [
...T,
...U,
] extends [...infer R]
? R
: never;
51 changes: 51 additions & 0 deletions packages/hono-do/tests/fixtures/alarm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Hono } from "hono";

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

const app = new Hono<{
Bindings: {
ALARM: DurableObjectNamespace;
};
}>();

app.all("/alarm/*", (c) => {
const id = c.env.ALARM.idFromName("alarm");
const obj = c.env.ALARM.get(id);
return obj.fetch(c.req.raw);
});

export const Alarm = generateHonoObject(
"/alarm",
async (app, { storage }, vars) => {
app.post("/", async (c) => {
const currentAlarm = await storage.getAlarm();
if (currentAlarm == null) {
await storage.setAlarm(Date.now() + 100);
}

return c.json({ queued: true });
});

app.get("/", async (c) => {
// NOTE:
// ```ts
// declare module "hono-do" {
// interface HonoObjectVars {
// message: string;
// }
// }
// ```
//
// This will resolve the type error, but this code effects other files. so I don't use this.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
return c.text(vars.message);
});
},
).alarm(async (state, vars) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
vars.message = "Hello, Hono DO!";
});

export default app;
9 changes: 9 additions & 0 deletions packages/hono-do/tests/fixtures/alarm/wrangler.toml
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 = "ALARM", class_name = "Alarm" }]

[[migrations]]
tag = "v1"
new_classes = ["Alarm"]
Loading