///
import { DurableObject } from 'cloudflare:workers'
import { layerProtocolDurableObject, toDurableObjectHandler } from '@livestore/common-cf'
import { Effect, Layer, Rpc, RpcClient, RpcGroup, RpcSerialization, Schema } from '@livestore/utils/effect'
export class TestRpcs extends RpcGroup.make(
Rpc.make('Ping', {
payload: Schema.Struct({ message: Schema.String }),
success: Schema.Struct({ response: Schema.String }),
}),
Rpc.make('Echo', {
payload: Schema.Struct({ text: Schema.String }),
success: Schema.Struct({ echo: Schema.String }),
}),
Rpc.make('Add', {
payload: Schema.Struct({ a: Schema.Number, b: Schema.Number }),
success: Schema.Struct({ result: Schema.Number }),
}),
) {}
export interface Env {
TEST_RPC_DO: DurableObjectNamespace
TEST_RPC_CLIENT_DO: DurableObjectNamespace
}
export class TestRpcDurableObject extends DurableObject {
__DURABLE_OBJECT_BRAND = 'TestRpcDurableObject' as never
async rpc(payload: unknown): Promise {
const TestRpcsLive = TestRpcs.toLayer({
Ping: ({ message }) => Effect.succeed({ response: `Pong: ${message}` }),
Echo: ({ text }) => Effect.succeed({ echo: `Echo: ${text}` }),
Add: ({ a, b }) => Effect.succeed({ result: a + b }),
})
return toDurableObjectHandler(TestRpcs, {
layer: Layer.mergeAll(TestRpcsLive, RpcSerialization.layerJson),
})(payload)
}
}
export class TestRpcClientDO extends DurableObject {
__DURABLE_OBJECT_BRAND = 'TestRpcClientDO' as never
readonly env: Env
constructor(state: DurableObjectState, env: Env) {
super(state, env)
this.env = env
}
async fetch(request: Request): Promise {
try {
const url = new URL(request.url)
if (url.pathname === '/call-server') {
const method = url.searchParams.get('method')
const serverDO = this.env.TEST_RPC_DO.get(this.env.TEST_RPC_DO.idFromName('test-server'))
// Create protocol layer for DO RPC communication
const ProtocolLive = layerProtocolDurableObject((payload) => serverDO.rpc(payload)).pipe(
Layer.provide(RpcSerialization.layerJson),
)
// Use idiomatic Effect RPC client pattern from README
const program = Effect.gen(function* () {
const client = yield* RpcClient.make(TestRpcs)
// Call RPC methods using clean API
switch (method) {
case 'ping': {
const message = url.searchParams.get('message') || 'Hello'
return yield* client.Ping({ message })
}
case 'echo': {
const text = url.searchParams.get('text') || 'Hello World'
return yield* client.Echo({ text })
}
case 'add': {
const a = Number.parseInt(url.searchParams.get('a') || '5')
const b = Number.parseInt(url.searchParams.get('b') || '3')
return yield* client.Add({ a, b })
}
default:
return yield* Effect.fail(new Error(`Unknown method: ${method}`))
}
}).pipe(Effect.scoped)
const result = await program.pipe(Effect.provide(ProtocolLive), Effect.runPromise)
return new Response(JSON.stringify({ success: true, result }), {
headers: { 'Content-Type': 'application/json' },
})
}
return new Response('Not found', { status: 404 })
} catch (error) {
return new Response(JSON.stringify({ success: false, error: String(error) }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
})
}
}
}
export default {
async fetch(request: Request, env: Env): Promise {
try {
const url = new URL(request.url)
// Idiomatic Effect RPC client testing
if (url.pathname === '/test-rpc-client') {
const clientDO = env.TEST_RPC_CLIENT_DO.get(env.TEST_RPC_CLIENT_DO.idFromName('test-client'))
const method = url.searchParams.get('method') ?? 'ping'
const clientUrl = new URL('/call-server', request.url)
clientUrl.searchParams.set('method', method)
// Forward parameters
for (const [key, value] of url.searchParams) {
if (key !== 'method') {
clientUrl.searchParams.set(key, value)
}
}
return clientDO.fetch(clientUrl.toString())
}
return new Response('Idiomatic Effect RPC Test\n\nEndpoints:\n- /test-rpc-client?method=ping|echo|add', {
headers: { 'Content-Type': 'text/plain' },
})
} catch (error) {
return new Response(`Error: ${error}`, { status: 500 })
}
},
}