Files
rose-ash/shared/sx/ref/boundary.sx
giles 17c58a2b5b Fix examples.sx: paren balance + dict eval crash at startup
1. Extra closing paren in ex-tabs handler
2. tab-content dict values contained (div ...) HTML tags which crash
   during register_components since HTML primitives aren't in env

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 01:31:10 +00:00

316 lines
8.6 KiB
Plaintext

;; ==========================================================================
;; boundary.sx — SX language boundary contract
;;
;; Declares the core I/O primitives that any SX host must provide.
;; This is the LANGUAGE contract — not deployment-specific.
;;
;; Pure primitives (Tier 1) are declared in primitives.sx.
;; Deployment-specific I/O lives in boundary-app.sx.
;; Per-service page helpers live in {service}/sx/boundary.sx.
;;
;; Format:
;; (define-io-primitive "name"
;; :params (param1 param2 &key ...)
;; :returns "type"
;; :effects [io]
;; :async true
;; :doc "description"
;; :context :request)
;;
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Tier 1: Pure primitives — declared in primitives.sx
;; --------------------------------------------------------------------------
(declare-tier :pure :source "primitives.sx")
;; --------------------------------------------------------------------------
;; Tier 2: Core I/O primitives — async, side-effectful, need host context
;;
;; These are generic web-platform I/O that any SX web host would provide,
;; regardless of deployment architecture.
;; --------------------------------------------------------------------------
;; Request context
(define-io-primitive "current-user"
:params ()
:returns "dict?"
:effects [io]
:async true
:doc "Current authenticated user dict, or nil."
:context :request)
(define-io-primitive "request-arg"
:params (name &rest default)
:returns "any"
:effects [io]
:async true
:doc "Read a query string argument from the current request."
:context :request)
(define-io-primitive "request-path"
:params ()
:returns "string"
:effects [io]
:async true
:doc "Current request path."
:context :request)
(define-io-primitive "request-view-args"
:params (key)
:returns "any"
:effects [io]
:async true
:doc "Read a URL view argument from the current request."
:context :request)
(define-io-primitive "csrf-token"
:params ()
:returns "string"
:effects [io]
:async true
:doc "Current CSRF token string."
:context :request)
(define-io-primitive "abort"
:params (status &rest message)
:returns "nil"
:effects [io]
:async true
:doc "Raise HTTP error from SX."
:context :request)
;; Routing
(define-io-primitive "url-for"
:params (endpoint &key)
:returns "string"
:effects [io]
:async true
:doc "Generate URL for a named endpoint."
:context :request)
(define-io-primitive "route-prefix"
:params ()
:returns "string"
:effects [io]
:async true
:doc "Service URL prefix for dev/prod routing."
:context :request)
;; Config and host context (sync — no await needed)
(define-io-primitive "app-url"
:params (service &rest path)
:returns "string"
:effects [io]
:async false
:doc "Full URL for a service: (app-url \"blog\" \"/my-post/\")."
:context :config)
(define-io-primitive "asset-url"
:params (&rest path)
:returns "string"
:effects [io]
:async false
:doc "Versioned static asset URL."
:context :config)
(define-io-primitive "config"
:params (key)
:returns "any"
:effects [io]
:async false
:doc "Read a value from host configuration."
:context :config)
;; --------------------------------------------------------------------------
;; Boundary types — what's allowed to cross the host-SX boundary
;; --------------------------------------------------------------------------
(define-boundary-types
(list "number" "string" "boolean" "nil" "keyword"
"list" "dict" "sx-source"))
;; --------------------------------------------------------------------------
;; Web interop — reading non-SX request formats
;;
;; SX's native wire format is SX (text/sx). These primitives bridge to
;; legacy web formats: HTML form encoding, JSON bodies, HTTP headers.
;; They're useful for interop but not fundamental to SX-to-SX communication.
;; --------------------------------------------------------------------------
(define-io-primitive "now"
:params (&rest format)
:returns "string"
:async true
:doc "Current timestamp. Optional format string (strftime). Default ISO 8601."
:context :request)
(define-io-primitive "sleep"
:params (ms)
:returns "nil"
:async true
:doc "Pause execution for ms milliseconds. For demos and testing."
:context :request)
(define-io-primitive "request-form"
:params (name &rest default)
:returns "any"
:async true
:doc "Read a form field from a POST/PUT/PATCH request body."
:context :request)
(define-io-primitive "request-json"
:params ()
:returns "dict?"
:async true
:doc "Read JSON body from the current request, or nil if not JSON."
:context :request)
(define-io-primitive "request-header"
:params (name &rest default)
:returns "string?"
:async true
:doc "Read a request header value by name."
:context :request)
(define-io-primitive "request-content-type"
:params ()
:returns "string?"
:async true
:doc "Content-Type of the current request."
:context :request)
(define-io-primitive "request-args-all"
:params ()
:returns "dict"
:async true
:doc "All query string parameters as a dict."
:context :request)
(define-io-primitive "request-form-all"
:params ()
:returns "dict"
:async true
:doc "All form fields as a dict."
:context :request)
(define-io-primitive "request-form-list"
:params (field-name)
:returns "list"
:async true
:doc "All values for a multi-value form field as a list."
:context :request)
(define-io-primitive "request-headers-all"
:params ()
:returns "dict"
:async true
:doc "All request headers as a dict (lowercase keys)."
:context :request)
(define-io-primitive "request-file-name"
:params (field-name)
:returns "string?"
:async true
:doc "Filename of an uploaded file by field name, or nil."
:context :request)
;; Response manipulation
(define-io-primitive "set-response-header"
:params (name value)
:returns "nil"
:async true
:doc "Set a response header. Applied after handler returns."
:context :request)
(define-io-primitive "set-response-status"
:params (status)
:returns "nil"
:async true
:doc "Set the HTTP response status code. Applied after handler returns."
:context :request)
;; Ephemeral state — per-process, resets on restart
(define-io-primitive "state-get"
:params (key &rest default)
:returns "any"
:async true
:doc "Read from ephemeral per-process state dict."
:context :request)
(define-io-primitive "state-set!"
:params (key value)
:returns "nil"
:async true
:doc "Write to ephemeral per-process state dict."
:context :request)
;; --------------------------------------------------------------------------
;; Tier 3: Signal primitives — reactive state for islands
;;
;; These are pure primitives (no IO) but are separated from primitives.sx
;; because they introduce a new type (signal) and depend on signals.sx.
;; --------------------------------------------------------------------------
(declare-tier :signals :source "signals.sx")
(declare-signal-primitive "signal"
:params (initial-value)
:returns "signal"
:effects []
:doc "Create a reactive signal container with an initial value.")
(declare-signal-primitive "deref"
:params (signal)
:returns "any"
:effects []
:doc "Read a signal's current value. In a reactive context (inside an island),
subscribes the current DOM binding to the signal. Outside reactive
context, just returns the value.")
(declare-signal-primitive "reset!"
:params (signal value)
:returns "nil"
:effects [mutation]
:doc "Set a signal to a new value. Notifies all subscribers.")
(declare-signal-primitive "swap!"
:params (signal f &rest args)
:returns "nil"
:effects [mutation]
:doc "Update a signal by applying f to its current value. (swap! s inc)
is equivalent to (reset! s (inc (deref s))) but atomic.")
(declare-signal-primitive "computed"
:params (compute-fn)
:returns "signal"
:effects []
:doc "Create a derived signal that recomputes when its dependencies change.
Dependencies are discovered automatically by tracking deref calls.")
(declare-signal-primitive "effect"
:params (effect-fn)
:returns "lambda"
:effects [mutation]
:doc "Run a side effect that re-runs when its signal dependencies change.
Returns a dispose function. If the effect function returns a function,
it is called as cleanup before the next run.")
(declare-signal-primitive "batch"
:params (thunk)
:returns "any"
:effects [mutation]
:doc "Group multiple signal writes. Subscribers are notified once at the end,
after all values have been updated.")