Remove redundant features: ref sugar, suspense, transitions

- ref/ref-get/ref-set! functions removed (just dict wrappers — use dict
  primitives directly). The :ref attribute stays in adapter-dom.sx.
- Suspense form removed (if/when + deref on resource signals covers it)
- Transition function removed (fine-grained signals already avoid jank)
- Kept: error-boundary, resource, portal, :ref attribute

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 16:54:40 +00:00
parent 7efd1b401b
commit 06adbdcd59
5 changed files with 39 additions and 270 deletions

View File

@@ -14,7 +14,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-08T16:36:52Z";
var SX_VERSION = "2026-03-08T16:54:18Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -1492,7 +1492,7 @@ return result; }, args);
return (isSxTruthy(skip) ? assoc(state, "skip", false, "i", (get(state, "i") + 1)) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() {
var attrName = keywordName(arg);
var attrVal = trampoline(evalExpr(nth(args, (get(state, "i") + 1)), env));
(isSxTruthy(sxOr(isNil(attrVal), (attrVal == false))) ? NIL : (isSxTruthy((isSxTruthy(startsWith(attrName, "on-")) && isCallable(attrVal))) ? domListen(el, slice(attrName, 3), attrVal) : (isSxTruthy((isSxTruthy((attrName == "bind")) && isSignal(attrVal))) ? bindInput(el, attrVal) : (isSxTruthy((attrName == "ref")) ? refSet_b(attrVal, el) : (isSxTruthy(contains(BOOLEAN_ATTRS, attrName)) ? (isSxTruthy(attrVal) ? domSetAttr(el, attrName, "") : NIL) : (isSxTruthy((attrVal == true)) ? domSetAttr(el, attrName, "") : domSetAttr(el, attrName, (String(attrVal)))))))));
(isSxTruthy(sxOr(isNil(attrVal), (attrVal == false))) ? NIL : (isSxTruthy((isSxTruthy(startsWith(attrName, "on-")) && isCallable(attrVal))) ? domListen(el, slice(attrName, 3), attrVal) : (isSxTruthy((isSxTruthy((attrName == "bind")) && isSignal(attrVal))) ? bindInput(el, attrVal) : (isSxTruthy((attrName == "ref")) ? dictSet(attrVal, "current", el) : (isSxTruthy(contains(BOOLEAN_ATTRS, attrName)) ? (isSxTruthy(attrVal) ? domSetAttr(el, attrName, "") : NIL) : (isSxTruthy((attrVal == true)) ? domSetAttr(el, attrName, "") : domSetAttr(el, attrName, (String(attrVal)))))))));
return assoc(state, "skip", true, "i", (get(state, "i") + 1));
})() : ((isSxTruthy(!isSxTruthy(contains(VOID_ELEMENTS, tag))) ? domAppend(el, renderToDom(arg, env, newNs)) : NIL), assoc(state, "i", (get(state, "i") + 1)))));
})(); }, {["i"]: 0, ["skip"]: false}, args);
@@ -1546,7 +1546,7 @@ return result; }, args);
var renderDomUnknownComponent = function(name) { return error((String("Unknown component: ") + String(name))); };
// RENDER_DOM_FORMS
var RENDER_DOM_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defisland", "defmacro", "defstyle", "defhandler", "map", "map-indexed", "filter", "for-each", "portal", "error-boundary", "suspense"];
var RENDER_DOM_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defisland", "defmacro", "defstyle", "defhandler", "map", "map-indexed", "filter", "for-each", "portal", "error-boundary"];
// render-dom-form?
var isRenderDomForm = function(name) { return contains(RENDER_DOM_FORMS, name); };
@@ -1604,7 +1604,7 @@ return result; }, args);
return domAppend(frag, val);
})(); }, coll);
return frag;
})() : (isSxTruthy((name == "filter")) ? renderToDom(trampoline(evalExpr(expr, env)), env, ns) : (isSxTruthy((name == "portal")) ? renderDomPortal(args, env, ns) : (isSxTruthy((name == "error-boundary")) ? renderDomErrorBoundary(args, env, ns) : (isSxTruthy((name == "suspense")) ? renderDomSuspense(args, env, ns) : (isSxTruthy((name == "for-each")) ? (function() {
})() : (isSxTruthy((name == "filter")) ? renderToDom(trampoline(evalExpr(expr, env)), env, ns) : (isSxTruthy((name == "portal")) ? renderDomPortal(args, env, ns) : (isSxTruthy((name == "error-boundary")) ? renderDomErrorBoundary(args, env, ns) : (isSxTruthy((name == "for-each")) ? (function() {
var f = trampoline(evalExpr(nth(expr, 1), env));
var coll = trampoline(evalExpr(nth(expr, 2), env));
var frag = createFragment();
@@ -1613,7 +1613,7 @@ return result; }, args);
return domAppend(frag, val);
})(); } }
return frag;
})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns))))))))))))))); };
})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns)))))))))))))); };
// render-lambda-dom
var renderLambdaDom = function(f, args, env, ns) { return (function() {
@@ -1805,30 +1805,6 @@ return (boundaryDisposers = []); });
})();
})(); };
// render-dom-suspense
var renderDomSuspense = function(args, env, ns) { return (function() {
var fallbackExpr = first(args);
var bodyExprs = rest(args);
var container = domCreateElement("div", NIL);
domSetAttr(container, "data-sx-suspense", "true");
domAppend(container, renderToDom(fallbackExpr, env, ns));
(function() {
var bodyDisposers = [];
effect(function() { { var _c = bodyDisposers; for (var _i = 0; _i < _c.length; _i++) { var d = _c[_i]; d(); } }
bodyDisposers = [];
return tryCatch(function() { return (function() {
var frag = createFragment();
withIslandScope(function(disposable) { bodyDisposers.push(disposable);
return registerInScope(disposable); }, function() { return forEach(function(child) { return domAppend(frag, renderToDom(child, env, ns)); }, bodyExprs); });
domSetProp(container, "innerHTML", "");
return domAppend(container, frag);
})(); }, function(err) { return NIL; }); });
return registerInScope(function() { { var _c = bodyDisposers; for (var _i = 0; _i < _c.length; _i++) { var d = _c[_i]; d(); } }
return (bodyDisposers = []); });
})();
return container;
})(); };
// === Transpiled from engine ===
@@ -3088,15 +3064,6 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
// register-in-scope
var registerInScope = function(disposable) { return (isSxTruthy(_islandScope) ? _islandScope(disposable) : NIL); };
// ref
var ref = function(initial) { return {["current"]: initial}; };
// ref-get
var refGet = function(r) { return get(r, "current"); };
// ref-set!
var refSet_b = function(r, v) { return dictSet(r, "current", v); };
// *store-registry*
var _storeRegistry = {};
@@ -3138,11 +3105,6 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
return state;
})(); };
// transition
var transition = function(pendingSig, thunk) { reset_b(pendingSig, true);
return scheduleIdle(function() { batch(thunk);
return reset_b(pendingSig, false); }); };
// =========================================================================
// Platform interface — DOM adapter (browser-only)

View File

@@ -14,7 +14,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-08T16:36:52Z";
var SX_VERSION = "2026-03-08T16:54:18Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -1492,7 +1492,7 @@ return result; }, args);
return (isSxTruthy(skip) ? assoc(state, "skip", false, "i", (get(state, "i") + 1)) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() {
var attrName = keywordName(arg);
var attrVal = trampoline(evalExpr(nth(args, (get(state, "i") + 1)), env));
(isSxTruthy(sxOr(isNil(attrVal), (attrVal == false))) ? NIL : (isSxTruthy((isSxTruthy(startsWith(attrName, "on-")) && isCallable(attrVal))) ? domListen(el, slice(attrName, 3), attrVal) : (isSxTruthy((isSxTruthy((attrName == "bind")) && isSignal(attrVal))) ? bindInput(el, attrVal) : (isSxTruthy((attrName == "ref")) ? refSet_b(attrVal, el) : (isSxTruthy(contains(BOOLEAN_ATTRS, attrName)) ? (isSxTruthy(attrVal) ? domSetAttr(el, attrName, "") : NIL) : (isSxTruthy((attrVal == true)) ? domSetAttr(el, attrName, "") : domSetAttr(el, attrName, (String(attrVal)))))))));
(isSxTruthy(sxOr(isNil(attrVal), (attrVal == false))) ? NIL : (isSxTruthy((isSxTruthy(startsWith(attrName, "on-")) && isCallable(attrVal))) ? domListen(el, slice(attrName, 3), attrVal) : (isSxTruthy((isSxTruthy((attrName == "bind")) && isSignal(attrVal))) ? bindInput(el, attrVal) : (isSxTruthy((attrName == "ref")) ? dictSet(attrVal, "current", el) : (isSxTruthy(contains(BOOLEAN_ATTRS, attrName)) ? (isSxTruthy(attrVal) ? domSetAttr(el, attrName, "") : NIL) : (isSxTruthy((attrVal == true)) ? domSetAttr(el, attrName, "") : domSetAttr(el, attrName, (String(attrVal)))))))));
return assoc(state, "skip", true, "i", (get(state, "i") + 1));
})() : ((isSxTruthy(!isSxTruthy(contains(VOID_ELEMENTS, tag))) ? domAppend(el, renderToDom(arg, env, newNs)) : NIL), assoc(state, "i", (get(state, "i") + 1)))));
})(); }, {["i"]: 0, ["skip"]: false}, args);
@@ -1546,7 +1546,7 @@ return result; }, args);
var renderDomUnknownComponent = function(name) { return error((String("Unknown component: ") + String(name))); };
// RENDER_DOM_FORMS
var RENDER_DOM_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defisland", "defmacro", "defstyle", "defhandler", "map", "map-indexed", "filter", "for-each", "portal", "error-boundary", "suspense"];
var RENDER_DOM_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defisland", "defmacro", "defstyle", "defhandler", "map", "map-indexed", "filter", "for-each", "portal", "error-boundary"];
// render-dom-form?
var isRenderDomForm = function(name) { return contains(RENDER_DOM_FORMS, name); };
@@ -1604,7 +1604,7 @@ return result; }, args);
return domAppend(frag, val);
})(); }, coll);
return frag;
})() : (isSxTruthy((name == "filter")) ? renderToDom(trampoline(evalExpr(expr, env)), env, ns) : (isSxTruthy((name == "portal")) ? renderDomPortal(args, env, ns) : (isSxTruthy((name == "error-boundary")) ? renderDomErrorBoundary(args, env, ns) : (isSxTruthy((name == "suspense")) ? renderDomSuspense(args, env, ns) : (isSxTruthy((name == "for-each")) ? (function() {
})() : (isSxTruthy((name == "filter")) ? renderToDom(trampoline(evalExpr(expr, env)), env, ns) : (isSxTruthy((name == "portal")) ? renderDomPortal(args, env, ns) : (isSxTruthy((name == "error-boundary")) ? renderDomErrorBoundary(args, env, ns) : (isSxTruthy((name == "for-each")) ? (function() {
var f = trampoline(evalExpr(nth(expr, 1), env));
var coll = trampoline(evalExpr(nth(expr, 2), env));
var frag = createFragment();
@@ -1613,7 +1613,7 @@ return result; }, args);
return domAppend(frag, val);
})(); } }
return frag;
})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns))))))))))))))); };
})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns)))))))))))))); };
// render-lambda-dom
var renderLambdaDom = function(f, args, env, ns) { return (function() {
@@ -1805,30 +1805,6 @@ return (boundaryDisposers = []); });
})();
})(); };
// render-dom-suspense
var renderDomSuspense = function(args, env, ns) { return (function() {
var fallbackExpr = first(args);
var bodyExprs = rest(args);
var container = domCreateElement("div", NIL);
domSetAttr(container, "data-sx-suspense", "true");
domAppend(container, renderToDom(fallbackExpr, env, ns));
(function() {
var bodyDisposers = [];
effect(function() { { var _c = bodyDisposers; for (var _i = 0; _i < _c.length; _i++) { var d = _c[_i]; d(); } }
bodyDisposers = [];
return tryCatch(function() { return (function() {
var frag = createFragment();
withIslandScope(function(disposable) { bodyDisposers.push(disposable);
return registerInScope(disposable); }, function() { return forEach(function(child) { return domAppend(frag, renderToDom(child, env, ns)); }, bodyExprs); });
domSetProp(container, "innerHTML", "");
return domAppend(container, frag);
})(); }, function(err) { return NIL; }); });
return registerInScope(function() { { var _c = bodyDisposers; for (var _i = 0; _i < _c.length; _i++) { var d = _c[_i]; d(); } }
return (bodyDisposers = []); });
})();
return container;
})(); };
// === Transpiled from engine ===
@@ -3088,15 +3064,6 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
// register-in-scope
var registerInScope = function(disposable) { return (isSxTruthy(_islandScope) ? _islandScope(disposable) : NIL); };
// ref
var ref = function(initial) { return {["current"]: initial}; };
// ref-get
var refGet = function(r) { return get(r, "current"); };
// ref-set!
var refSet_b = function(r, v) { return dictSet(r, "current", v); };
// *store-registry*
var _storeRegistry = {};
@@ -3138,11 +3105,6 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
return state;
})(); };
// transition
var transition = function(pendingSig, thunk) { reset_b(pendingSig, true);
return scheduleIdle(function() { batch(thunk);
return reset_b(pendingSig, false); }); };
// =========================================================================
// Platform interface — DOM adapter (browser-only)

View File

@@ -187,7 +187,7 @@
(bind-input el attr-val)
;; ref: set ref.current to this element
(= attr-name "ref")
(ref-set! attr-val el)
(dict-set! attr-val "current" el)
;; Boolean attr
(contains? BOOLEAN_ATTRS attr-name)
(when attr-val (dom-set-attr el attr-name ""))
@@ -310,7 +310,7 @@
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
"map" "map-indexed" "filter" "for-each" "portal"
"error-boundary" "suspense"))
"error-boundary"))
(define render-dom-form?
(fn (name)
@@ -435,10 +435,6 @@
(= name "error-boundary")
(render-dom-error-boundary args env ns)
;; suspense — show fallback while resource is loading
(= name "suspense")
(render-dom-suspense args env ns)
;; for-each (render variant)
(= name "for-each")
(let ((f (trampoline (eval-expr (nth expr 1) env)))
@@ -828,66 +824,6 @@
container))))
;; --------------------------------------------------------------------------
;; render-dom-suspense — show fallback while resource is loading
;; --------------------------------------------------------------------------
;;
;; (suspense fallback-expr body...)
;;
;; Renders fallback-expr initially. When used with a resource signal,
;; an effect watches the resource state and swaps in the body content
;; once loading is complete. If the resource errors, renders the error.
;;
;; The simplest pattern: wrap a resource deref in suspense.
;;
;; (suspense
;; (div "Loading...")
;; (let ((data (get (deref user-resource) "data")))
;; (div (get data "name"))))
(define render-dom-suspense
(fn (args env ns)
(let ((fallback-expr (first args))
(body-exprs (rest args))
(container (dom-create-element "div" nil)))
(dom-set-attr container "data-sx-suspense" "true")
;; Render fallback immediately
(dom-append container (render-to-dom fallback-expr env ns))
;; Try to render body — if it works, replace fallback
;; The body typically derefs a resource signal, which triggers
;; an effect that re-renders when the resource resolves
(let ((body-disposers (list)))
(effect (fn ()
;; Dispose previous body renders
(for-each (fn (d) (d)) body-disposers)
(set! body-disposers (list))
;; Try rendering the body
(try-catch
(fn ()
(let ((frag (create-fragment)))
(with-island-scope
(fn (disposable)
(append! body-disposers disposable)
(register-in-scope disposable))
(fn ()
(for-each
(fn (child)
(dom-append frag (render-to-dom child env ns)))
body-exprs)))
;; Success — replace container content with body
(dom-set-prop container "innerHTML" "")
(dom-append container frag)))
(fn (err)
;; Body threw — keep showing fallback (or show error)
nil))))
;; Register cleanup
(register-in-scope
(fn ()
(for-each (fn (d) (d)) body-disposers)
(set! body-disposers (list)))))
container)))
;; --------------------------------------------------------------------------
;; Platform interface — DOM adapter
;; --------------------------------------------------------------------------
@@ -944,7 +880,6 @@
;; From signals.sx:
;; signal, deref, reset!, swap!, computed, effect, batch
;; signal?, with-island-scope, register-in-scope
;; ref, ref-get, ref-set!
;;
;; Pure primitives used:
;; keys, get, str

View File

@@ -306,28 +306,7 @@
;; ==========================================================================
;; 12. Refs — mutable boxes, no reactivity
;; ==========================================================================
;;
;; A ref is a mutable container that does NOT trigger subscriptions when
;; written. Like React's useRef: holds mutable values between renders, and
;; provides imperative DOM element access via :ref attribute.
(define ref
(fn (initial)
(dict "current" initial)))
(define ref-get
(fn (r)
(get r "current")))
(define ref-set!
(fn (r v)
(dict-set! r "current" v)))
;; ==========================================================================
;; 13. Named stores — page-level signal containers (L3)
;; 12. Named stores — page-level signal containers (L3)
;; ==========================================================================
;;
;; Stores persist across island creation/destruction. They live at page
@@ -411,7 +390,7 @@
;; ==========================================================================
;; 15. Resource — async signal with loading/resolved/error states
;; 14. Resource — async signal with loading/resolved/error states
;; ==========================================================================
;;
;; A resource wraps an async operation (fetch, computation) and exposes
@@ -440,27 +419,3 @@
state)))
;; ==========================================================================
;; 16. Transitions — non-urgent signal updates
;; ==========================================================================
;;
;; Transitions mark updates as non-urgent. The thunk's signal writes are
;; deferred to an idle callback, keeping the UI responsive during expensive
;; computations.
;;
;; (transition pending-signal thunk)
;;
;; Sets pending-signal to true, schedules thunk via requestIdleCallback
;; (or setTimeout 0 as fallback), then sets pending-signal to false when
;; the thunk completes. Signal writes inside the thunk are batched.
;;
;; Platform interface required:
;; (schedule-idle thunk) → void — requestIdleCallback or setTimeout(fn, 0)
(define transition
(fn (pending-sig thunk)
(reset! pending-sig true)
(schedule-idle
(fn ()
(batch thunk)
(reset! pending-sig false)))))