cloudflare-workers

Proper Use of Cloudflare Durable Objects


I'm looking to use Durable Objects in my app. I chose them over KV because I need to do a series of writes, and I found that KV wasn't consistent enough for my needs. The patterns in the docs for DOs (fetch syntax and awaiting Class properties) feels foreign to me. Anyone care to provide suggestions/feedback on my Class design?

Everything seems to work ok... but I wanted to get some second opinions on my patterns.

// siteData.ts
import { Env } from 'hono';
import { SiteInfo } from './types';
import { DurableObject } from 'cloudflare:workers';

export class SiteData extends DurableObject {
    constructor(state: DurableObjectState, env: Env) {
        super(state, env);
    }

    async initialize(siteId: string, credits = 15, isPro = false, access_token: string) {
        if (!siteId) {
            throw new Error('siteId is required');
        }
        if (!access_token) {
            throw new Error('access_token is required');
        }
        if (isPro) {
            credits = 100;
        }
        const createdAt = new Date().toISOString();

        // This feels wrong for some reason :)
        await this.ctx.storage.put('siteId', siteId);
        await this.ctx.storage.put('access_token', access_token);
        await this.ctx.storage.put('credits', credits);
        await this.ctx.storage.put('isPro', isPro);
        await this.ctx.storage.put('createdAt', createdAt);

        const siteInfo: SiteInfo = {
            siteId,
            access_token,
            credits,
            isPro,
            createdAt,
        };

        return siteInfo;
    }

    async setSiteId(siteId: string) {
        await this.ctx.storage.put('siteId', siteId);
    }

    async getSiteId() {
        let siteId: string | undefined = (await this.ctx.storage.get('siteId')) || undefined;
        return siteId;
    }

    async getCredits() {
        let credits: number = (await this.ctx.storage.get('credits')) || 0;
        return credits;
    }

// hono API index.ts
// imports and middleware removed

app.get('/register', async (c) => {
    const { siteId, access_token } = c.req.query();
    if (!siteId || !access_token) {
        c.status(400);
        return c.json({ message: 'No siteId or access token provided.' });
    }

    const siteInfoDO = c.env.SITE_DATA_DO.idFromName(siteId);
    const stub = c.env.SITE_DATA_DO.get(siteInfoDO);

    const siteInfo = await stub.initialize(siteId, 15, false, access_token);

    return c.json({ message: 'Success!', siteInfo });
});

on interesting thing is that TS is telling me the siteInfo object return by stub.initialize() is never type. I'm wondering if I misinterpreted the docs? Should I be defining fetch inside the Durable Object and calling that instead?

Another worry of mine is long term maintenance of my App state with Durable Objects. With Workers KV, we have a rudimentary UI to perform basic CRUD operations in Workers dashboard, and we can interact with KV through wrangler. Durable Objects don't have any of this functionality so I'd have to provide customer support through my own API, which is fine... just want to confirm it.

Thank you!


Solution

  • You seem to have things mostly right.

    The TypeScript errors are probably because you haven't fully specified the type of the SITE_DATA_DO binding. This wasn't part of the code you pasted, but I assume you've defined its type as just DurableObjectNamespace. To get type checking of RPCs, you need to make it DurableObjectNamespace<SiteData>, that is, specify your server-side class (or the interface it implements) as a type parameter. More info here.

    Regarding this:

    // This feels wrong for some reason :)
    

    I would suggest, instead of writing the site info as 5 different key/values, you could write the whole SiteInfo object as a single value. Values can be any structured clonable type, so you can store a whole object at once, you don't need to break it up.

    This does mean that in setSiteId(), you would need to read/modify/write, but that's probably fine, especially given that the system does in-memory caching automatically.

    With Workers KV, we have a rudimentary UI to perform basic CRUD operations in Workers dashboard, and we can interact with KV through wrangler. Durable Objects don't have any of this functionality so I'd have to provide customer support through my own API, which is fine... just want to confirm it.

    Yes, Durable Objects are intended as more of a low-level building block compared to KV, and are less "batteries-included". That said, it's quite possible we (Cloudflare) will build a storage explorer UI at some point.