Commit610df2b5Recorded13 Jul 2026Repositoryenclave
enclave-rpc: add browser/WASM driver (rpc-browser) over sigil-irc-websocket
Message
The Slate-side client driver: poll-driven (no blocking reads) to suit the browser's async WebSocket. rpc-browser-poll registers NICK/USER once the socket is really OPEN (irc-ws-connected? — the socket reports open optimistically before it connects, per the transport-spike handshake finding), then drains inbound frames into the session core and executes the emitted actions. No multi-chunk send pacing (a client issues single small requests). sigil-irc-websocket moves to a regular dependency so the module compiles in the normal build; a consumer imports only the driver (native or browser) it needs.
Changed
packages/enclave-rpc/package.sgl | 13 ++++++++-----
packages/enclave-rpc/src/enclave/rpc/browser.sgl | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 150 insertions(+), 5 deletions(-)Diff
packages/enclave-rpc/package.sglmodified
@@ -19,15 +19,18 @@
19
dependencies: (list 20
(from-git url: "codeberg:sigil/sigil-irc" 21
package: "sigil-irc"−22
version: "^0.16.0"))−23
−24
dev-dependencies: (list−25
(from-git url: "codeberg:sigil/sigil" package: "sigil-test" version: "^0.17.0")−26
(from-git url: "codeberg:sigil/sigil" package: "sigil-test-runner" version: "^0.17.0")+22
version: "^0.16.0")+23
;; The browser/WASM driver ((enclave rpc browser)) rides this; the+24
;; native driver rides (sigil irc) above. A consumer imports only the+25
;; driver module it needs. 26
(from-git url: "codeberg:sigil/sigil-irc" 27
package: "sigil-irc-websocket" 28
version: "^0.16")) 29
+30
dev-dependencies: (list+31
(from-git url: "codeberg:sigil/sigil" package: "sigil-test" version: "^0.17.0")+32
(from-git url: "codeberg:sigil/sigil" package: "sigil-test-runner" version: "^0.17.0"))+33
34
tasks: (list 35
(task 36
name: 'buildpackages/enclave-rpc/src/enclave/rpc/browser.sgladded
@@ -0,0 +1,142 @@
+1
;;; (enclave rpc browser) — browser/WASM driver: run an `rpc-session`+2
;;; over a `(sigil irc websocket)` session, advanced by a poll tick.+3
;;;+4
;;; The browser's WebSocket is asynchronous, so this driver is strictly+5
;;; poll-driven (no blocking reads): a host-side timer calls+6
;;; `rpc-browser-poll` on a tick, which (1) sends NICK/USER once the+7
;;; socket is actually OPEN — `irc-ws-connect` optimistically reports+8
;;; `open` before the browser socket finishes connecting, so we gate on+9
;;; `irc-ws-connected?` (see the transport-spike ★ handshake finding) —+10
;;; and (2) drains inbound frames, feeding each to the session core and+11
;;; executing the actions it emits.+12
;;;+13
;;; This is the client the Slate inspector uses. A client issues single,+14
;;; small `!rpc` requests (it doesn't chunk), so unlike the native host+15
;;; driver there is no multi-chunk send pacing to do here.+16
+17
(define-library (enclave rpc browser)+18
(import (sigil core)+19
(sigil struct)+20
(sigil irc message)+21
(sigil irc websocket)+22
(enclave rpc core))+23
+24
(export+25
rpc-browser rpc-browser?+26
rpc-browser-ws rpc-browser-session+27
make-rpc-browser+28
rpc-browser-connect+29
rpc-browser-poll+30
rpc-browser-request!+31
rpc-browser-eval! rpc-browser-inspect!+32
rpc-browser-connected?+33
rpc-browser-quit)+34
+35
(begin+36
+37
(define-struct rpc-browser+38
(ws) ; the (sigil irc websocket) session+39
(session) ; the rpc-session core+40
(registered default: #f mutable: #t) ; sent NICK/USER yet?+41
(on-deliver default: #f)+42
(on-error default: #f)+43
(on-note default: #f))+44
+45
;; Execute one core action against the WebSocket session.+46
(define (execute-action! drv action)+47
(let ((ws (rpc-browser-ws drv))+48
(kind (action-kind action)))+49
(cond+50
((eq? kind 'privmsg)+51
(irc-ws-privmsg ws (privmsg-target action) (privmsg-text action)))+52
((eq? kind 'join)+53
(irc-ws-join ws (join-channel action)))+54
((eq? kind 'deliver)+55
(let ((cb (rpc-browser-on-deliver drv)))+56
(when cb (cb (deliver-label action) (deliver-value action)))))+57
((eq? kind 'rpc-err)+58
(let ((cb (rpc-browser-on-error drv)))+59
(when cb (cb (rpc-err-label action) (rpc-err-message action)))))+60
((eq? kind 'note)+61
(let ((cb (rpc-browser-on-note drv)))+62
(when cb (cb (note-text action)))))+63
(else #f))))+64
+65
(define (execute! drv actions)+66
(for-each (lambda (a) (execute-action! drv a)) actions))+67
+68
(define (feed! drv msg)+69
(execute! drv (rpc-on-message (rpc-browser-session drv) msg)))+70
+71
;; Build a driver over an existing (unconnected/connecting) ws session.+72
(define (make-rpc-browser (keys: (ws #f)+73
(session #f)+74
(on-deliver #f)+75
(on-error #f)+76
(on-note #f)))+77
(unless ws (error "make-rpc-browser: ws: is required"))+78
(unless session (error "make-rpc-browser: session: is required"))+79
(rpc-browser+80
ws: ws+81
session: session+82
on-deliver: on-deliver+83
on-error: on-error+84
on-note: on-note))+85
+86
;; Open a WebSocket for SESSION and build the driver. Does not block+87
;; on the socket becoming OPEN — `rpc-browser-poll` sends registration+88
;; once `irc-ws-connected?` is true.+89
(define (rpc-browser-connect (keys: (url #f)+90
(session #f)+91
(on-deliver #f)+92
(on-error #f)+93
(on-note #f)))+94
(unless url (error "rpc-browser-connect: url: is required"))+95
(unless session (error "rpc-browser-connect: session: is required"))+96
(let ((ws (irc-ws-connect url: url nick: (rpc-session-self-nick session))))+97
(and ws+98
(make-rpc-browser+99
ws: ws session: session+100
on-deliver: on-deliver on-error: on-error on-note: on-note))))+101
+102
;; Advance the driver one tick: register once the socket is open, then+103
;; drain up to `budget` inbound frames. Call on a timer. Returns #t+104
;; while the session is live, #f once the socket has closed.+105
(define (rpc-browser-poll drv (keys: (budget 64)))+106
(let ((ws (rpc-browser-ws drv)))+107
(cond+108
((not ws) #f)+109
(else+110
;; Send NICK/USER once, when the browser socket is really OPEN.+111
(when (and (not (rpc-browser-registered drv))+112
(irc-ws-connected? ws))+113
(set-rpc-browser-registered! drv #t)+114
(irc-ws-register ws))+115
;; Drain inbound frames.+116
(let loop ((n 0) (live #t))+117
(if (or (>= n budget) (not live))+118
live+119
(let ((msg (irc-ws-receive ws)))+120
(cond+121
((irc-message? msg) (feed! drv msg) (loop (+ n 1) #t))+122
((eq? msg 'closed) #f) ; socket closed+123
((eq? msg #f) #t) ; nothing pending this tick+124
(else (loop (+ n 1) #t))))))))))+125
+126
;; Issue a request and send it now. Returns the correlation label.+127
(define (rpc-browser-request! drv verb form)+128
(let ((r (rpc-request! (rpc-browser-session drv) verb form)))+129
(execute! drv (cdr r))+130
(car r)))+131
+132
(define (rpc-browser-eval! drv form) (rpc-browser-request! drv "eval" form))+133
(define (rpc-browser-inspect! drv form) (rpc-browser-request! drv "inspect" form))+134
+135
(define (rpc-browser-connected? drv)+136
(let ((ws (rpc-browser-ws drv)))+137
(and ws (irc-ws-connected? ws))))+138
+139
(define (rpc-browser-quit drv . message)+140
(apply irc-ws-quit (rpc-browser-ws drv) message))+141
+142
))