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: 'build
packages/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
))