Last active
March 27, 2024 22:10
-
-
Save colelawrence/ba912db3cd1539ee820c3fe5e67654df to your computer and use it in GitHub Desktop.
Create strongly typed Durable Objects with TRPC wrappers (requires trpc@10)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createTRPCProxyClient, httpLink } from "@trpc/client"; | |
import { AnyRouter, initTRPC, MaybePromise, Router } from "@trpc/server"; | |
import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; | |
import type { ParserWithInputOutput, ParserWithoutInput } from "@trpc/server/dist/core/parser"; | |
import type { AnyRouterDef } from "@trpc/server/dist/core/router"; | |
import type { ResponseMetaFn } from "@trpc/server/dist/http/internals/types"; | |
import { getParseFn } from "./getParseFn"; | |
export type TrpcDurableObjectRouter<Router extends AnyRouter> = { | |
router: Router, | |
/** Allow mutating the response */ | |
responseMeta?: ResponseMetaFn<Router>; | |
} | |
export interface TRPCDurableObjectConstructor<Env, Router extends AnyRouter> { | |
new( | |
controller: DurableObjectState, | |
env: Env, | |
): DurableObject | |
getClient(getStub: () => DurableObjectStub): ReturnType<typeof createTRPCProxyClient<Router>> | |
} | |
export type DurableObjectContext<Env> = { | |
env: Env | |
} | |
type AnyTRPCDurableObjectRouter<Env> = Router<AnyRouterDef<DurableObjectContext<Env>>>; | |
export function initTRPCDurableObject<Env>(envParser?: ParserWithoutInput<Env> | ParserWithInputOutput<unknown, Env>) { | |
const envParserFn = envParser ? getParseFn(envParser) : undefined | |
return { | |
t: initTRPC<{ ctx: DurableObjectContext<Env> }>()( | |
// Is this transformer + errorFormatter something we should allow configuring? | |
), | |
object: function createTRPCDurableObject<R extends AnyTRPCDurableObjectRouter<Env>>( | |
createObject: ( | |
state: DurableObjectState, | |
env: Env, | |
/** indentity function to help with type checking */ | |
defineReturn: <X extends TrpcDurableObjectRouter<AnyTRPCDurableObjectRouter<Env>>>(value: X) => X | |
) => Promise<TrpcDurableObjectRouter<R>> | |
): TRPCDurableObjectConstructor<Env, R> { | |
const DUMMY_ENDPOINT = "/trpc"; | |
return class TRPCDurableObjectClass { | |
public _fetch: Promise<(req: Request) => Promise<Response>> | |
public static getClient(getStub: () => MaybePromise<DurableObjectStub>) { | |
let stub: undefined | DurableObjectStub | |
return createTRPCProxyClient<any>({ | |
async fetch(...args) { | |
if (!stub) stub = await Promise.resolve(getStub()) | |
// logic borrowed from the rate limiter in `cloudflare/workers-chat-demo` | |
// https://github.com/cloudflare/workers-chat-demo/blob/b4462b1be2dd110c313ed7c9baed92044e03181c/src/chat.mjs#L510-L542 | |
try { | |
// attempt fetch on the stub we have | |
return await stub.fetch(...args); | |
} catch (err) { | |
// `fetch()` threw an exception. This is probably because the limiter has been | |
// disconnected. Stubs implement E-order semantics, meaning that calls to the same stub | |
// are delivered to the remote object in order, until the stub becomes disconnected, after | |
// which point all further calls fail. This guarantee makes a lot of complex interaction | |
// patterns easier, but it means we must be prepared for the occasional disconnect, as | |
// networks are inherently unreliable. | |
// | |
// Anyway, get a new stub and try again. If it fails again, something else is probably | |
// wrong. | |
stub = await Promise.resolve(getStub()); | |
return await stub.fetch(...args); | |
} | |
}, | |
links: [ | |
httpLink({ | |
// need to actually use a middleware for redirecting the fetch request | |
// see https://discord.com/channels/595317990191398933/1010005243548934227/1010020619993239623 | |
url: `http://dummy-url` + DUMMY_ENDPOINT, | |
}) | |
], | |
}) as any; // as any return to avoid TypeScript "excessvely deep type instantiation" | |
} | |
constructor( | |
state: DurableObjectState, | |
env: Env | |
) { | |
// hmm... | |
if (envParserFn) env = envParserFn(env) as any | |
this._fetch = state.blockConcurrencyWhile(async () => { | |
const trpcObj = await Promise.resolve(createObject(state, env, identity)) | |
// const createContext = trpcObj.createContext.bind(trpcObj); | |
return async (req) => { | |
const res = await fetchRequestHandler<AnyRouter>({ | |
endpoint: DUMMY_ENDPOINT, | |
req, | |
router: trpcObj.router, | |
responseMeta: trpcObj.responseMeta, | |
// createContext, | |
}); | |
return res; | |
}; | |
}) | |
} | |
async fetch(req: Request): Promise<Response> { | |
return (await this._fetch)(req); | |
} | |
} | |
} | |
} | |
} | |
function identity<T>(x: T): T { | |
return x | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export type ParseFn<T> = (value: unknown) => T | Promise<T>; | |
export function getParseFn<T>(procedureParser: unknown): ParseFn<T> { | |
const parser = procedureParser as any; | |
if (typeof parser === 'function') { | |
// ProcedureParserCustomValidatorEsque | |
return parser; | |
} | |
if (typeof parser.parseAsync === 'function') { | |
// ProcedureParserZodEsque | |
return parser.parseAsync.bind(parser); | |
} | |
if (typeof parser.parse === 'function') { | |
// ProcedureParserZodEsque | |
return parser.parse.bind(parser); | |
} | |
if (typeof parser.validateSync === 'function') { | |
// ProcedureParserYupEsque | |
return parser.validateSync.bind(parser); | |
} | |
if (typeof parser.create === 'function') { | |
// ProcedureParserSuperstructEsque | |
return parser.create.bind(parser); | |
} | |
throw new Error('Could not find a validator fn'); | |
} |
@maurerbot, I use something like this for an MVP project that's built around Durable Objects for storage. I don't recall the differences between what's posted here and what exists in my codebase at this time. I've added several features like rate limiting, open telemetry reporting, and durable storage adapters with zod with it.
The codebase I have has diverged a lot from this sample, so it's hard to recreate an example.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
how do you use this? Example DO and stub creation?