import { Context, Effect, type Layer } from "effect"; import type { Schema as S } from "effect"; import * as Binding from "./Binding"; import * as DurableObjectEntrypoint from "./DurableObject"; import type { DurableObjectHandler } from "./DurableObject"; import * as DurableObjectNamespace from "./DurableObjectNamespace"; import type { DurableObjectState } from "./DurableObjectState"; import type * as Rpc from "./Rpc"; import * as RpcDefinition from "./RpcDefinition"; import type { WorkerEnvironment } from "./Environment"; export type ServiceFreeSchema = S.Codec; export interface Method< Args extends ReadonlyArray = ReadonlyArray, Success extends ServiceFreeSchema = ServiceFreeSchema, > { readonly args: Args; readonly success: Success; } export namespace Method { export type Any = Method, ServiceFreeSchema>; type ArgsFromSchemas> = Args extends readonly [] ? [] : Args extends readonly [ infer Head extends ServiceFreeSchema, ...infer Tail extends ReadonlyArray, ] ? [S.Schema.Type, ...ArgsFromSchemas] : Array>; type EncodedArgsFromSchemas> = Args extends readonly [] ? [] : Args extends readonly [ infer Head extends ServiceFreeSchema, ...infer Tail extends ReadonlyArray, ] ? [S.Codec.Encoded, ...EncodedArgsFromSchemas] : Array>; export type Args = ArgsFromSchemas; export type EncodedArgs = EncodedArgsFromSchemas; export type Success = S.Schema.Type; export type EncodedSuccess = S.Codec.Encoded; } export type Methods = Record; /** * RPC contract for a Durable Object class. */ export interface Definition { readonly id: Id; readonly methods: MethodsShape; } export namespace Definition { export type Any = Definition; } export type ReservedMethodName = | RpcDefinition.ReservedMethodName | "fetch" | "alarm" | "webSocketMessage" | "webSocketClose" | "webSocketError"; export type NoReservedMethods = Extract extends never ? MethodsShape : never; const reservedMethodNames = new Set([ "constructor", "dup", "fetch", "alarm", "webSocketMessage", "webSocketClose", "webSocketError", ]); /** * Promise-based client API derived from a Durable Object definition. */ export type ServerApi = { readonly [Key in keyof Self["methods"]]: ( ...args: Method.Args ) => Promise>; }; export type Api = Rpc.Provider, ReservedMethodName>; /** * Effect handlers for each RPC method in a Durable Object definition. */ export type Handlers = { readonly [Key in keyof Self["methods"]]: ( ...args: Method.Args ) => DurableObjectHandler>; }; type BoundaryHandlers = { readonly [Key in keyof Self["methods"]]: ( ...args: Array ) => DurableObjectHandler>; }; /** * Durable Object constructor options for a specific RPC definition. */ export interface Options< ROut, Self extends Definition.Any, REvent = never, EventLayerError = never, > extends Omit< DurableObjectEntrypoint.DurableObjectOptions< ROut, REvent, EventLayerError, Handlers >, "rpc" > { readonly rpc: Handlers; } export type LayerOptions = { readonly binding: string; }; export type TagClass = Context.ServiceClass< Self, Id, DurableObjectNamespace.DurableObjectNamespaceEffectClient< Api>, Definition > > & DurableObjectNamespace.DurableObjectNamespaceStaticClient< Self, Api>, Definition > & { readonly id: Id; readonly methods: MethodsShape; readonly make: ( layer: Layer.Layer, options: Options, REvent, EventLayerError>, ) => DurableObjectEntrypoint.DurableObjectClass< Handlers>, ROut | REvent >; readonly layer: ( options: LayerOptions, ) => Layer.Layer< Self, Binding.BindingNotFoundError | Binding.BindingValidationError, WorkerEnvironment >; }; /** * Defines a single RPC method schema in a Durable Object definition. */ export const method = RpcDefinition.method as { (definition: { readonly success: Success; }): Method; < const Args extends ReadonlyArray, Success extends ServiceFreeSchema, >(definition: { readonly args: Args; readonly success: Success; }): Method; }; /** * Creates a Durable Object RPC definition plus implementation/binding helpers. * * @example * ```ts * const ChatRoom = DurableObjectDefinition.make("ChatRoom", { * postMessage: DurableObjectDefinition.method({ * args: [Schema.String], * success: Schema.Void, * }), * }); * ``` */ const makeDefinition = ( id: Id, methods: MethodsShape & NoReservedMethods, ) => { type SelfDefinition = Definition; RpcDefinition.assertNoReservedMethods("Durable Object", methods, reservedMethodNames); const definition: SelfDefinition = RpcDefinition.make(id, methods); return Object.assign(definition, { make: ( layer: Layer.Layer, options: Options, ) => DurableObjectEntrypoint.make(layer, { ...options, rpc: wrapHandlers(definition, options.rpc), }), }); }; export const make = ( id: Id, methods: MethodsShape & NoReservedMethods, ) => Tag>()( id, methods as MethodsShape & NoReservedMethods, ); export const Tag = () => ( id: Id, methods: MethodsShape & NoReservedMethods, ) => { const definition = makeDefinition(id, methods); type SelfDefinition = Definition; type ClientApi = Api; const tag = Context.Service< Self, DurableObjectNamespace.DurableObjectNamespaceEffectClient >()(id); const bindingDefinition = (binding: LayerOptions) => ({ ...binding, definition, }); const layer = (binding: LayerOptions) => DurableObjectNamespace.layer( tag, bindingDefinition(binding), ); const newUniqueId = Effect.fnUntraced(function* ( options?: globalThis.DurableObjectNamespaceNewUniqueIdOptions, ) { const namespace = yield* tag; return yield* namespace.newUniqueId(options); }); const idFromName = Effect.fnUntraced(function* (name: string) { const namespace = yield* tag; return yield* namespace.idFromName(name); }); const idFromString = Effect.fnUntraced(function* (value: string) { const namespace = yield* tag; return yield* namespace.idFromString(value); }); const get = Effect.fnUntraced(function* ( objectId: globalThis.DurableObjectId, options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions, ) { const namespace = yield* tag; return yield* namespace.get(objectId, options); }); const getByName = Effect.fnUntraced(function* ( name: string, options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions, ) { const namespace = yield* tag; return yield* namespace.getByName(name, options); }); const jurisdiction = Effect.fnUntraced(function* (value: globalThis.DurableObjectJurisdiction) { const namespace = yield* tag; return yield* namespace.jurisdiction(value); }); const fetch = ( stub: DurableObjectNamespace.DurableObjectStubClient, input: RequestInfo | URL, init?: RequestInit, ) => Effect.gen(function* () { const namespace = yield* tag; return yield* namespace.fetch(stub, input, init); }); const rpc = ( stub: DurableObjectNamespace.DurableObjectStubClient, method: Method, ...args: ClientApi[Method] extends (...args: infer Args) => unknown ? Args : never ) => Effect.gen(function* () { const namespace = yield* tag; return yield* namespace.rpc(stub, method as never, ...(args as never)); }); const call = ( stub: DurableObjectNamespace.DurableObjectStubClient, method: Method, ...args: ClientApi[Method] extends (...args: infer Args) => unknown ? Args : never ) => Effect.gen(function* () { const namespace = yield* tag; return yield* namespace.call(stub, method as never, ...(args as never)); }); const scopedCall = ( stub: DurableObjectNamespace.DurableObjectStubClient, method: Method, ...args: ClientApi[Method] extends (...args: infer Args) => unknown ? Args : never ) => Effect.gen(function* () { const namespace = yield* tag; return yield* namespace.scopedCall(stub, method as never, ...(args as never)); }); const unsafeRaw = Effect.fnUntraced(function* () { const namespace = yield* tag; return yield* namespace.unsafeRaw; }); const directMethods = DurableObjectNamespace.makeDirectMethods( definition, { call: call as never, fetch, get, getByName, }, ); return Object.assign(tag, directMethods, { id: definition.id, methods: definition.methods, make: definition.make, layer, newUniqueId, idFromName, idFromString, get, getByName, jurisdiction, fetch, rpc, call, scopedCall, unsafeRaw, }) as unknown as TagClass; }; export const DurableObject = Tag; const wrapHandlers = ( definition: Self, handlers: Handlers, ): BoundaryHandlers => { const wrapped = {} as Record; for (const key of Object.keys(definition.methods) as Array< RpcDefinition.Definition.MethodNames >) { const handler = handlers[key]; wrapped[key] = (...args: Array) => Effect.gen(function* () { const decodedArgs = yield* RpcDefinition.decodeArgs(definition, key, args); const value = yield* handler(...decodedArgs); return yield* RpcDefinition.encodeSuccess(definition, key, value); }); } return wrapped as BoundaryHandlers; }; /** * Helper for implementing handlers with the exact method shape of a definition. */ export const implement = ( _definition: Self, handlers: Handlers, ): Handlers => handlers; /** * Convenience alias for a single Durable Object RPC handler Effect. */ export type HandlerEffect< ROut, Self extends Definition.Any, Key extends keyof Self["methods"], > = DurableObjectHandler>;