kwhitley / itty-durable

Cloudflare Durable Objects + Itty Router = shorter code
MIT License
236 stars 18 forks source link

Usage with `Remix.run` #18

Closed AlCalzone closed 1 year ago

AlCalzone commented 2 years ago

This library looks really cool and solves a pain point I've been having with DOs from the start. Unfortunately, I'm using https://remix.run in my project, which has a file-based routing convention, so I'm at a loss how I can hook the withDurables middleware in there.

I should be able to modify the request context using getLoadContext, similar to https://remix.run/docs/en/v1/other-api/adapter, so a more "manual" version of this magic hook could help here. Is there something like that?

maurerbot commented 1 year ago

I was able to use the proxyDurable method withDurables uses. @AlCalzone see https://github.com/kwhitley/itty-durable/issues/31 for example

AlCalzone commented 1 year ago

I got this working using withDurables after all. It is a bit awkward though, since we have to individually wrap each loader function and cannot hook this up in createRequestHandler in the CF workers adapter, because Remix passes a new Request instance to each loader.

Wrap Remix loader function with the following function:

// in any route using a loader:
export const loader = loaderWithDurables(async (args) => {
  // loader code
});

// Definition of loaderWithDurables:
export function loaderWithDurables(fn: LoaderFunctionWithDurables): LoaderFunction {
    return (args) => {
        withDurables({ parse: true })(args.request, args.context.env);
        return fn(args as any);
    };
}

The types are defined as follows:

export type LoaderFunctionArgs = Omit<DataFunctionArgs, "context"> & { context: Context };

export type LoaderFunction = (
    args: LoaderFunctionArgs,
) => Response | null | Promise<Response | null>;

export type LoaderFunctionWithDurablesArgs = { request: RequestWithDurables } & Omit<DataFunctionArgs, "context" | "request"> & { context: Context };

export type LoaderFunctionWithDurables = (
    args: LoaderFunctionWithDurablesArgs,
) => Response | null | Promise<Response | null>;

export interface CloudflareEnvironment {
    // ... everything that should be available under `context.env`
}

export interface Context {
    ctx: ExecutionContext;
    env: CloudflareEnvironment;
}

The RequestWithDurables mimicks what itty-durable does to the request object:

export interface RequestWithDurables extends Request, TestProps /*, ... other DO props */ {}

Each XyzProps typelooks like this:

export type TestProps = {
    TEST: IttyDurableObjectNamespace<TestObj>;
}

where TEST is the env key, and TestObj is the class name.

type PromisifyPublicFunctions<T> = {
    [K in keyof T]: T[K] extends (...args: any[]) => any
        ? (...args: Parameters<T[K]>) => Promise<Awaited<ReturnType<T[K]>>>
        : never;
};

interface IttyDurableObjectNamespace<T> {
    get(id: string | DurableObjectId): PromisifyPublicFunctions<T>;
}