Last active
January 13, 2026 14:30
-
-
Save dmmulroy/9b4b0a5cad92941885cdfc6e304a0d39 to your computer and use it in GitHub Desktop.
Durable Object stub wrapper with better-result hydration
This file contains hidden or 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
| // example-usage.ts | |
| import { Result } from "better-result"; | |
| import { stubWithBetterResultHydration } from "./stub-with-better-result-hydration"; | |
| import type { MyDurableObject } from "./my-durable-object"; | |
| // In a Worker handler: | |
| export default { | |
| async fetch(req: Request, env: Env) { | |
| const id = env.MY_DO.idFromName("user-123"); | |
| const rawStub = env.MY_DO.get(id); | |
| // Wrap the stub once | |
| const stub = stubWithBetterResultHydration<MyDurableObject>(rawStub); | |
| // All methods now return Result with DurableObjectError in error union | |
| const result = await stub.createUser({ name: "Alice" }); | |
| if (result.isErr()) { | |
| // result.error is ParseError | ValidationError | DurableObjectError | |
| return new Response(result.error.message, { status: 500 }); | |
| } | |
| // result.value is User | |
| return Response.json(result.value); | |
| } | |
| }; | |
| // Or with Result.gen: | |
| const handleRequest = async (env: Env) => { | |
| const stub = stubWithBetterResultHydration<MyDurableObject>( | |
| env.MY_DO.get(env.MY_DO.idFromName("user-123")) | |
| ); | |
| return Result.gen(async function* () { | |
| const user = yield* Result.await(stub.createUser({ name: "Alice" })); | |
| const profile = yield* Result.await(stub.getProfile(user.id)); | |
| return Result.ok({ user, profile }); | |
| }); | |
| }; |
This file contains hidden or 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
| // stub-with-better-result-hydration.ts | |
| import { DurableObjectError } from "./errors"; | |
| import { Err, Ok, Result, type ResultType } from "better-result"; | |
| /** | |
| * Adds DurableObjectError to Result error unions, wraps non-Result returns. | |
| * - Promise<Result<T, E>> → Promise<Result<T, E | DurableObjectError>> | |
| * - Promise<T> → Promise<Result<T, DurableObjectError>> | |
| */ | |
| type AddDOError<R> = | |
| R extends Promise<ResultType<infer T, infer E>> | |
| ? Promise<ResultType<T, E | DurableObjectError>> | |
| : R extends Promise<infer T> | |
| ? Promise<ResultType<T, DurableObjectError>> | |
| : R; | |
| /** | |
| * Maps DO stub methods, adding DurableObjectError to all returns. | |
| * All async methods return Result with DurableObjectError in error union. | |
| */ | |
| type HydratedStub<T> = { | |
| [K in keyof T]: T[K] extends (...args: infer P) => infer R ? (...args: P) => AddDOError<R> : T[K]; | |
| }; | |
| /** | |
| * Wraps a Durable Object stub to: | |
| * 1. Rehydrate serialized Result returns (Ok/Err class instances) | |
| * 2. Catch DO infrastructure errors and wrap in DurableObjectError | |
| * 3. Wrap non-Result returns in Ok | |
| * | |
| * All methods return Result with DurableObjectError in error union. | |
| * | |
| * IMPORTANT: After any DO exception, stub may be broken. | |
| * Subsequent requests on same stub may fail. Recreate stub after errors. | |
| * | |
| * @param stub - DO stub to wrap | |
| * @returns Proxied stub with Result rehydration and error handling | |
| * | |
| * @example | |
| * const stub = stubWithBetterResultHydration(namespace.get(id)); | |
| * const result = await stub.createUser(input); // Result<User, ParseError | DurableObjectError> | |
| */ | |
| export const stubWithBetterResultHydration = <T extends Rpc.DurableObjectBranded>( | |
| stub: DurableObjectStub<T>, | |
| ): HydratedStub<T> => { | |
| return new Proxy(stub, { | |
| get(target, prop, receiver) { | |
| const value = Reflect.get(target, prop, receiver); | |
| if (typeof value !== "function") { | |
| return value; | |
| } | |
| return async (...args: unknown[]) => { | |
| let result: unknown; | |
| try { | |
| result = await value.apply(target, args); | |
| } catch (e) { | |
| // DO infrastructure error - wrap in DurableObjectError | |
| return new Err(DurableObjectError.from(e, String(prop))); | |
| } | |
| // Rehydrate serialized Results back into Ok/Err instances | |
| const hydrated = Result.hydrate(result); | |
| if (hydrated) { | |
| return hydrated; | |
| } | |
| // Non-Result return - wrap in Ok | |
| return new Ok(result); | |
| }; | |
| }, | |
| }) as unknown as HydratedStub<T>; | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment