@@ -76,6 +76,136 @@ exp: nowInSeconds + 1000,
7676})
7777```
7878
79+ ## RPC
80+
81+ ` iso-ucan/rpc ` is a small request/response layer on top of UCAN
82+ invocations. You declare a protocol once as a record of commands
83+ (` defineCommand ` ), then build matching client and server pairs from it
84+ (` defineClient ` / ` defineServer ` ). Every command's success and error shapes
85+ are described by a single receipt schema (` receipt ` , ` receiptResult ` ,
86+ ` receiptError ` ) which is shared by both ends, so the call sites are fully
87+ type-safe and discriminated unions just work on the client.
88+
89+ Internal server failures (invalid invocation, unknown command, handler
90+ threw, …) are surfaced as a generic ` SERVER_ERROR ` variant that
91+ ` receipt(...) ` adds implicitly, so clients can always handle them with one
92+ narrow.
93+
94+ ### Define a shared protocol
95+
96+ ``` ts
97+ import {
98+ defineCommand ,
99+ receipt ,
100+ receiptError ,
101+ receiptResult ,
102+ } from ' iso-ucan/rpc'
103+ import { z } from ' zod'
104+
105+ const TodoSchema = z .object ({
106+ id: z .string (),
107+ text: z .string (),
108+ done: z .boolean (),
109+ })
110+
111+ export const Protocol = {
112+ TodoList: defineCommand ({
113+ cmd: ' /todo/list' ,
114+ args: z .object ({}),
115+ receipt: receipt (receiptResult (z .object ({ todos: z .array (TodoSchema ) }))),
116+ }),
117+ TodoAdd: defineCommand ({
118+ cmd: ' /todo/add' ,
119+ args: z .object ({ text: z .string ().min (1 ) }),
120+ receipt: receipt (receiptResult (z .object ({ todo: TodoSchema }))),
121+ }),
122+ TodoComplete: defineCommand ({
123+ cmd: ' /todo/complete' ,
124+ args: z .object ({ id: z .string () }),
125+ receipt: receipt (
126+ receiptResult (z .object ({ todo: TodoSchema })),
127+ receiptError (' NOT_FOUND' , ' Todo not found.' , z .object ({ id: z .string () }))
128+ ),
129+ }),
130+ } as const
131+ ```
132+
133+ ### Server (e.g. Hono + ` @hono/node-server ` )
134+
135+ ``` ts
136+ import { serve } from ' @hono/node-server'
137+ import { Hono } from ' hono'
138+ import { MemoryDriver } from ' iso-kv/drivers/memory.js'
139+ import { defineServer , type ServerHandlers } from ' iso-ucan/rpc'
140+ import { Store } from ' iso-ucan/store'
141+ import { Protocol } from ' ./protocol.ts'
142+
143+ const store = new Store (new MemoryDriver ())
144+ // ...seed `store` with the delegations that authorise your clients...
145+
146+ const handlers: ServerHandlers <typeof Protocol > = {
147+ ' /todo/list' : ({ invocation }) => ({
148+ cid: invocation .cid ,
149+ result: { todos: [/* ... */ ] },
150+ }),
151+ ' /todo/add' : ({ args , invocation }) => ({
152+ cid: invocation .cid ,
153+ result: { todo: { id: ' 1' , text: args .text , done: false } },
154+ }),
155+ ' /todo/complete' : ({ args , invocation }) => {
156+ // return either { cid, result: ... } or { cid, error: { code, message, data } }
157+ return {
158+ cid: invocation .cid ,
159+ error: {
160+ code: ' NOT_FOUND' ,
161+ message: ' Todo not found.' ,
162+ data: { id: args .id },
163+ },
164+ }
165+ },
166+ }
167+
168+ const rpc = defineServer (Protocol , {
169+ signer: serverSigner ,
170+ store ,
171+ verifierResolver ,
172+ handlers ,
173+ })
174+
175+ const app = new Hono ()
176+ app .post (' /rpc' , (c ) => rpc (c .req .raw ))
177+ serve ({ fetch: app .fetch , port: 3000 })
178+ ```
179+
180+ ### Client
181+
182+ ``` ts
183+ import { defineClient } from ' iso-ucan/rpc'
184+
185+ const client = defineClient (Protocol , {
186+ url: ' http://localhost:3000/rpc' ,
187+ issuer: cliSigner ,
188+ audience: serverSigner .didObject ,
189+ store ,
190+ verifierResolver ,
191+ })
192+
193+ // `args` and the returned receipt are typed from the protocol entry for `cmd`.
194+ const r = await client .request ({ cmd: ' /todo/complete' , args: { id: ' 42' } })
195+
196+ if (' result' in r ) {
197+ console .log (r .result .todo )
198+ } else if (r .error .code === ' NOT_FOUND' ) {
199+ console .error (r .error .message , r .error .data ) // data: { id: string }
200+ } else {
201+ // r.error.code === 'SERVER_ERROR' — always present in every receipt union.
202+ console .error (' server error:' , r .error .message )
203+ }
204+ ```
205+
206+ A complete runnable example (Hono server + a small CLI client) lives in
207+ [ ` examples/rpc-todo ` ] ( ../../examples/rpc-todo ) .
208+
79209## License
80210
81211MIT © [ Hugo Dias] ( http://hugodias.me )
0 commit comments