sor4chi / hono-do

A wrapper of Cloudflare Workers's Durable Object for Hono.
https://www.npmjs.com/package/hono-do
MIT License
73 stars 2 forks source link

Durable Objects syntax idea #1

Closed sor4chi closed 9 months ago

sor4chi commented 10 months ago

Durable Object is a very good representation in that it can store state and mutations as a single object. However, if we could provide a third party, we would be able to ensure more flexibility and maintainability of notation.

ref: https://github.com/honojs/examples/issues/86

For now, I implemented the simplest method I can think of at the moment to generate a HonoObject in main and a sample of it.

As far as I know, Durable Object works by exporting the class itself, not the class instance. Therefore, we tried to use the Constructor Function to generate dynamic classes while keeping the Hono interface easy to use.

If you have any ideas for a better interface, we'd love to hear your opinions and suggestions!

sor4chi commented 10 months ago

Current Interface:

export const Counter = generateHonoObject("/counter", (app, state) => {
  const { storage } = state;

  app.post("/increment", async (c) => {
    const newVal = 1 + ((await storage.get<number>("value")) || 0);
    storage.put("value", newVal);
    return c.text(newVal.toString());
  });

  app.post("/decrement", async (c) => {
    const newVal = -1 + ((await storage.get<number>("value")) || 0);
    storage.put("value", newVal);
    return c.text(newVal.toString());
  });

  app.get("/", async (c) => {
    const value = (await storage.get<number>("value")) || 0;
    return c.text(value.toString());
  });
});
sor4chi commented 10 months ago

Hono has an API called Variable in its context, and we are trying to map the contents of storage to this API to see if we can somehow achieve a clean interface.

I tried to build a simpler vue-like interface by using Proxy to define getter and setter, but it seems to be unusable because Variable is Readonly.

In the first place, get/set of storage itself is all Promise return value, so it seems impossible to make it Proxy.

sor4chi commented 10 months ago

In #2, try to add React-like way to define the state! How about this?

geelen commented 10 months ago

Personally I really like this:

export const Counter = generateHonoObject("/counter", (app, state) => {
  const { storage } = state;

  app.post("/increment", async (c) => {
    const newVal = 1 + ((await storage.get<number>("value")) || 0);
    storage.put("value", newVal);
    return c.text(newVal.toString());
  });

  app.post("/decrement", async (c) => {
    const newVal = -1 + ((await storage.get<number>("value")) || 0);
    storage.put("value", newVal);
    return c.text(newVal.toString());
  });

  app.get("/", async (c) => {
    const value = (await storage.get<number>("value")) || 0;
    return c.text(value.toString());
  });
});

My thought would be to make the (app, state) => { callback async, then when you construct the object, wrap the Hono construction in blockConcurrencyWhile (see the example here: https://developers.cloudflare.com/durable-objects/learning/in-memory-state/)

That way you could do:

export const Counter = generateHonoObject("/counter", async (app, state) => {
  const { storage } = state;
  const config = await storage.get('__CONFIG') ?? {}

  app.post('/init', initValidator, async (c) => {
    Object.assign(config, await c.req.json())
    storage.put('__CONFIG', config)
  })

  app.use('/private/*',
    basicAuth({
      username: config.username,
      password: config.password,
    })
  )

  // ...
})

Does that make sense?

sor4chi commented 10 months ago

Hi @geelen, I'm glad you like it. That's a nice idea. I want to add it soon!

this is what you mean, right?

export function generateHonoObject<
  E extends Env = Env,
  S extends Schema = Record<string, never>,
  BasePath extends string = "/",
>(
  basePath: string,
- cb: (app: Hono<E, S, BasePath>, state: DurableObjectState) => void,
+ cb: (app: Hono<E, S, BasePath>, state: DurableObjectState) => Promise<void>,
) {
  const honoObject = function (
    this: HonoObject<E, S, BasePath>,
    state: DurableObjectState,
  ) {
    const app = new Hono<E, S, BasePath>().basePath(basePath);
    this.app = app;
-   cb(app, state);
+   state.blockConcurrencyWhile(async () => {
+     await cb(app, state);
+   });
  };

  honoObject.prototype.fetch = function (
    this: HonoObject<E, S, BasePath>,
    request: Request,
  ) {
    return this.app.fetch(request);
  };

  return honoObject;
}
sor4chi commented 9 months ago

Hi, @geelen I released your blockConcurrencyWhile idea since it is a backward compatible PR. Thank you so much. You can use this as hono-do@0.1.0!