Add suspense, resource, and transitions — Phase 2 complete
- suspense render-dom form: shows fallback while resource loads, swaps to body content when resource signal resolves - resource async signal: wraps promise into signal with loading/data/error dict, auto-transitions on resolve/reject via promise-then - transition: defers signal writes to requestIdleCallback, sets pending signal for UI feedback during expensive operations - Added schedule-idle, promise-then platform functions - All Phase 2 features now marked Done in status tables Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||||
var SX_VERSION = "2026-03-08T16:35:00Z";
|
var SX_VERSION = "2026-03-08T16:36:52Z";
|
||||||
|
|
||||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||||
@@ -1546,7 +1546,7 @@ return result; }, args);
|
|||||||
var renderDomUnknownComponent = function(name) { return error((String("Unknown component: ") + String(name))); };
|
var renderDomUnknownComponent = function(name) { return error((String("Unknown component: ") + String(name))); };
|
||||||
|
|
||||||
// RENDER_DOM_FORMS
|
// 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"];
|
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"];
|
||||||
|
|
||||||
// render-dom-form?
|
// render-dom-form?
|
||||||
var isRenderDomForm = function(name) { return contains(RENDER_DOM_FORMS, name); };
|
var isRenderDomForm = function(name) { return contains(RENDER_DOM_FORMS, name); };
|
||||||
@@ -1604,7 +1604,7 @@ return result; }, args);
|
|||||||
return domAppend(frag, val);
|
return domAppend(frag, val);
|
||||||
})(); }, coll);
|
})(); }, coll);
|
||||||
return frag;
|
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 == "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 == "suspense")) ? renderDomSuspense(args, env, ns) : (isSxTruthy((name == "for-each")) ? (function() {
|
||||||
var f = trampoline(evalExpr(nth(expr, 1), env));
|
var f = trampoline(evalExpr(nth(expr, 1), env));
|
||||||
var coll = trampoline(evalExpr(nth(expr, 2), env));
|
var coll = trampoline(evalExpr(nth(expr, 2), env));
|
||||||
var frag = createFragment();
|
var frag = createFragment();
|
||||||
@@ -1613,7 +1613,7 @@ return result; }, args);
|
|||||||
return domAppend(frag, val);
|
return domAppend(frag, val);
|
||||||
})(); } }
|
})(); } }
|
||||||
return frag;
|
return frag;
|
||||||
})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns)))))))))))))); };
|
})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns))))))))))))))); };
|
||||||
|
|
||||||
// render-lambda-dom
|
// render-lambda-dom
|
||||||
var renderLambdaDom = function(f, args, env, ns) { return (function() {
|
var renderLambdaDom = function(f, args, env, ns) { return (function() {
|
||||||
@@ -1805,6 +1805,30 @@ 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 ===
|
// === Transpiled from engine ===
|
||||||
|
|
||||||
@@ -3114,6 +3138,11 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
|||||||
return state;
|
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)
|
// Platform interface — DOM adapter (browser-only)
|
||||||
@@ -3406,6 +3435,11 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
|||||||
|
|
||||||
function promiseResolve(val) { return Promise.resolve(val); }
|
function promiseResolve(val) { return Promise.resolve(val); }
|
||||||
|
|
||||||
|
function promiseThen(p, onResolve, onReject) {
|
||||||
|
if (!p || !p.then) return p;
|
||||||
|
return onReject ? p.then(onResolve, onReject) : p.then(onResolve);
|
||||||
|
}
|
||||||
|
|
||||||
function promiseCatch(p, fn) { return p && p.catch ? p.catch(fn) : p; }
|
function promiseCatch(p, fn) { return p && p.catch ? p.catch(fn) : p; }
|
||||||
|
|
||||||
// --- Abort controllers ---
|
// --- Abort controllers ---
|
||||||
@@ -3849,6 +3883,10 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
|||||||
function errorMessage(e) {
|
function errorMessage(e) {
|
||||||
return e && e.message ? e.message : String(e);
|
return e && e.message ? e.message : String(e);
|
||||||
}
|
}
|
||||||
|
function scheduleIdle(fn) {
|
||||||
|
if (typeof requestIdleCallback !== "undefined") requestIdleCallback(fn);
|
||||||
|
else setTimeout(fn, 0);
|
||||||
|
}
|
||||||
function elementValue(el) { return el && el.value !== undefined ? el.value : NIL; }
|
function elementValue(el) { return el && el.value !== undefined ? el.value : NIL; }
|
||||||
|
|
||||||
function domAddListener(el, event, fn, opts) {
|
function domAddListener(el, event, fn, opts) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||||
var SX_VERSION = "2026-03-08T16:35:00Z";
|
var SX_VERSION = "2026-03-08T16:36:52Z";
|
||||||
|
|
||||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||||
@@ -1546,7 +1546,7 @@ return result; }, args);
|
|||||||
var renderDomUnknownComponent = function(name) { return error((String("Unknown component: ") + String(name))); };
|
var renderDomUnknownComponent = function(name) { return error((String("Unknown component: ") + String(name))); };
|
||||||
|
|
||||||
// RENDER_DOM_FORMS
|
// 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"];
|
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"];
|
||||||
|
|
||||||
// render-dom-form?
|
// render-dom-form?
|
||||||
var isRenderDomForm = function(name) { return contains(RENDER_DOM_FORMS, name); };
|
var isRenderDomForm = function(name) { return contains(RENDER_DOM_FORMS, name); };
|
||||||
@@ -1604,7 +1604,7 @@ return result; }, args);
|
|||||||
return domAppend(frag, val);
|
return domAppend(frag, val);
|
||||||
})(); }, coll);
|
})(); }, coll);
|
||||||
return frag;
|
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 == "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 == "suspense")) ? renderDomSuspense(args, env, ns) : (isSxTruthy((name == "for-each")) ? (function() {
|
||||||
var f = trampoline(evalExpr(nth(expr, 1), env));
|
var f = trampoline(evalExpr(nth(expr, 1), env));
|
||||||
var coll = trampoline(evalExpr(nth(expr, 2), env));
|
var coll = trampoline(evalExpr(nth(expr, 2), env));
|
||||||
var frag = createFragment();
|
var frag = createFragment();
|
||||||
@@ -1613,7 +1613,7 @@ return result; }, args);
|
|||||||
return domAppend(frag, val);
|
return domAppend(frag, val);
|
||||||
})(); } }
|
})(); } }
|
||||||
return frag;
|
return frag;
|
||||||
})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns)))))))))))))); };
|
})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns))))))))))))))); };
|
||||||
|
|
||||||
// render-lambda-dom
|
// render-lambda-dom
|
||||||
var renderLambdaDom = function(f, args, env, ns) { return (function() {
|
var renderLambdaDom = function(f, args, env, ns) { return (function() {
|
||||||
@@ -1805,6 +1805,30 @@ 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 ===
|
// === Transpiled from engine ===
|
||||||
|
|
||||||
@@ -3114,6 +3138,11 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
|||||||
return state;
|
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)
|
// Platform interface — DOM adapter (browser-only)
|
||||||
@@ -3406,6 +3435,11 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
|||||||
|
|
||||||
function promiseResolve(val) { return Promise.resolve(val); }
|
function promiseResolve(val) { return Promise.resolve(val); }
|
||||||
|
|
||||||
|
function promiseThen(p, onResolve, onReject) {
|
||||||
|
if (!p || !p.then) return p;
|
||||||
|
return onReject ? p.then(onResolve, onReject) : p.then(onResolve);
|
||||||
|
}
|
||||||
|
|
||||||
function promiseCatch(p, fn) { return p && p.catch ? p.catch(fn) : p; }
|
function promiseCatch(p, fn) { return p && p.catch ? p.catch(fn) : p; }
|
||||||
|
|
||||||
// --- Abort controllers ---
|
// --- Abort controllers ---
|
||||||
@@ -3849,6 +3883,10 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
|||||||
function errorMessage(e) {
|
function errorMessage(e) {
|
||||||
return e && e.message ? e.message : String(e);
|
return e && e.message ? e.message : String(e);
|
||||||
}
|
}
|
||||||
|
function scheduleIdle(fn) {
|
||||||
|
if (typeof requestIdleCallback !== "undefined") requestIdleCallback(fn);
|
||||||
|
else setTimeout(fn, 0);
|
||||||
|
}
|
||||||
function elementValue(el) { return el && el.value !== undefined ? el.value : NIL; }
|
function elementValue(el) { return el && el.value !== undefined ? el.value : NIL; }
|
||||||
|
|
||||||
function domAddListener(el, event, fn, opts) {
|
function domAddListener(el, event, fn, opts) {
|
||||||
|
|||||||
@@ -309,7 +309,8 @@
|
|||||||
(define RENDER_DOM_FORMS
|
(define RENDER_DOM_FORMS
|
||||||
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
|
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
|
||||||
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
|
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
|
||||||
"map" "map-indexed" "filter" "for-each" "portal" "error-boundary"))
|
"map" "map-indexed" "filter" "for-each" "portal"
|
||||||
|
"error-boundary" "suspense"))
|
||||||
|
|
||||||
(define render-dom-form?
|
(define render-dom-form?
|
||||||
(fn (name)
|
(fn (name)
|
||||||
@@ -434,6 +435,10 @@
|
|||||||
(= name "error-boundary")
|
(= name "error-boundary")
|
||||||
(render-dom-error-boundary args env ns)
|
(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)
|
;; for-each (render variant)
|
||||||
(= name "for-each")
|
(= name "for-each")
|
||||||
(let ((f (trampoline (eval-expr (nth expr 1) env)))
|
(let ((f (trampoline (eval-expr (nth expr 1) env)))
|
||||||
@@ -823,6 +828,66 @@
|
|||||||
container))))
|
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
|
;; Platform interface — DOM adapter
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -449,6 +449,7 @@ class JSEmitter:
|
|||||||
"dom-focus": "domFocus",
|
"dom-focus": "domFocus",
|
||||||
"try-catch": "tryCatch",
|
"try-catch": "tryCatch",
|
||||||
"error-message": "errorMessage",
|
"error-message": "errorMessage",
|
||||||
|
"schedule-idle": "scheduleIdle",
|
||||||
"element-value": "elementValue",
|
"element-value": "elementValue",
|
||||||
"validate-for-request": "validateForRequest",
|
"validate-for-request": "validateForRequest",
|
||||||
"with-transition": "withTransition",
|
"with-transition": "withTransition",
|
||||||
@@ -3031,6 +3032,11 @@ PLATFORM_ORCHESTRATION_JS = """
|
|||||||
|
|
||||||
function promiseResolve(val) { return Promise.resolve(val); }
|
function promiseResolve(val) { return Promise.resolve(val); }
|
||||||
|
|
||||||
|
function promiseThen(p, onResolve, onReject) {
|
||||||
|
if (!p || !p.then) return p;
|
||||||
|
return onReject ? p.then(onResolve, onReject) : p.then(onResolve);
|
||||||
|
}
|
||||||
|
|
||||||
function promiseCatch(p, fn) { return p && p.catch ? p.catch(fn) : p; }
|
function promiseCatch(p, fn) { return p && p.catch ? p.catch(fn) : p; }
|
||||||
|
|
||||||
// --- Abort controllers ---
|
// --- Abort controllers ---
|
||||||
@@ -3474,6 +3480,10 @@ PLATFORM_ORCHESTRATION_JS = """
|
|||||||
function errorMessage(e) {
|
function errorMessage(e) {
|
||||||
return e && e.message ? e.message : String(e);
|
return e && e.message ? e.message : String(e);
|
||||||
}
|
}
|
||||||
|
function scheduleIdle(fn) {
|
||||||
|
if (typeof requestIdleCallback !== "undefined") requestIdleCallback(fn);
|
||||||
|
else setTimeout(fn, 0);
|
||||||
|
}
|
||||||
function elementValue(el) { return el && el.value !== undefined ? el.value : NIL; }
|
function elementValue(el) { return el && el.value !== undefined ? el.value : NIL; }
|
||||||
|
|
||||||
function domAddListener(el, event, fn, opts) {
|
function domAddListener(el, event, fn, opts) {
|
||||||
|
|||||||
@@ -438,3 +438,29 @@
|
|||||||
(fn (data) (reset! state (dict "loading" false "data" data "error" nil)))
|
(fn (data) (reset! state (dict "loading" false "data" data "error" nil)))
|
||||||
(fn (err) (reset! state (dict "loading" false "data" nil "error" err))))
|
(fn (err) (reset! state (dict "loading" false "data" nil "error" err))))
|
||||||
state)))
|
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)))))
|
||||||
|
|||||||
@@ -149,7 +149,19 @@
|
|||||||
(td :class "px-3 py-2 text-stone-700" "Phase 2 remaining")
|
(td :class "px-3 py-2 text-stone-700" "Phase 2 remaining")
|
||||||
(td :class "px-3 py-2 text-stone-500 font-medium" "P2")
|
(td :class "px-3 py-2 text-stone-500 font-medium" "P2")
|
||||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500"
|
(td :class "px-3 py-2 font-mono text-xs text-stone-500"
|
||||||
(a :href "/reactive-islands/phase2" :sx-get "/reactive-islands/phase2" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "Error boundaries, suspense, transitions")))))))))
|
(a :href "/reactive-islands/phase2" :sx-get "/reactive-islands/phase2" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "Error boundaries, suspense, transitions")))
|
||||||
|
(tr :class "border-b border-stone-100"
|
||||||
|
(td :class "px-3 py-2 text-stone-700" "Error boundaries")
|
||||||
|
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
||||||
|
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx: error-boundary render-dom form"))
|
||||||
|
(tr :class "border-b border-stone-100"
|
||||||
|
(td :class "px-3 py-2 text-stone-700" "Suspense + resource")
|
||||||
|
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
||||||
|
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx, signals.sx"))
|
||||||
|
(tr
|
||||||
|
(td :class "px-3 py-2 text-stone-700" "Transitions")
|
||||||
|
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
||||||
|
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "signals.sx: transition, schedule-idle"))))))))
|
||||||
|
|
||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
;; Live demo islands
|
;; Live demo islands
|
||||||
@@ -407,12 +419,8 @@
|
|||||||
(p :class "mt-2 text-sm text-stone-500" "Run: " (code "python3 shared/sx/tests/run.py signals")))
|
(p :class "mt-2 text-sm text-stone-500" "Run: " (code "python3 shared/sx/tests/run.py signals")))
|
||||||
|
|
||||||
(~doc-section :title "What's next" :id "next"
|
(~doc-section :title "What's next" :id "next"
|
||||||
(p "Phase 1 and Phase 2 P0/P1 features are complete. The remaining P2 features are optional enhancements:")
|
(p "Phase 1 and Phase 2 are complete. The reactive islands system now includes: signals, effects, computed values, islands, disposal, stores, event bridges, reactive DOM rendering, input binding, keyed reconciliation, refs, portals, error boundaries, suspense, resource, and transitions.")
|
||||||
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
|
(p "See the " (a :href "/reactive-islands/phase2" :sx-get "/reactive-islands/phase2" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "Phase 2 plan") " for the full feature list and design details."))))
|
||||||
(li (strong "Error boundaries") " — catch errors in island subtrees, render fallback UI")
|
|
||||||
(li (strong "Suspense + resource") " — async-aware rendering with loading states")
|
|
||||||
(li (strong "Transitions") " — non-urgent signal updates for expensive re-renders"))
|
|
||||||
(p "See the " (a :href "/reactive-islands/phase2" :sx-get "/reactive-islands/phase2" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "Phase 2 plan") " for details."))))
|
|
||||||
|
|
||||||
|
|
||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
@@ -660,11 +668,14 @@
|
|||||||
(td :class "px-3 py-2 text-stone-700" "Refs + portals")
|
(td :class "px-3 py-2 text-stone-700" "Refs + portals")
|
||||||
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
||||||
(td :class "px-3 py-2 text-stone-700" "ref, ref-get, ref-set!, :ref, portal"))
|
(td :class "px-3 py-2 text-stone-700" "ref, ref-get, ref-set!, :ref, portal"))
|
||||||
|
(tr :class "border-b border-stone-100"
|
||||||
|
(td :class "px-3 py-2 text-stone-700" "Error boundaries + suspense")
|
||||||
|
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
||||||
|
(td :class "px-3 py-2 text-stone-700" "error-boundary, suspense, resource"))
|
||||||
(tr
|
(tr
|
||||||
(td :class "px-3 py-2 text-stone-700" "Phase 2 remaining")
|
(td :class "px-3 py-2 text-stone-700" "Transitions")
|
||||||
(td :class "px-3 py-2 text-violet-700 font-medium"
|
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
||||||
(a :href "/reactive-islands/phase2" :sx-get "/reactive-islands/phase2" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "Details →"))
|
(td :class "px-3 py-2 text-stone-700" "signals.sx: transition, schedule-idle"))))))
|
||||||
(td :class "px-3 py-2 text-stone-700" "Error boundaries, suspense, transitions"))))))
|
|
||||||
|
|
||||||
(~doc-section :title "Design Principles" :id "principles"
|
(~doc-section :title "Design Principles" :id "principles"
|
||||||
(ol :class "space-y-3 text-stone-600 list-decimal list-inside"
|
(ol :class "space-y-3 text-stone-600 list-decimal list-inside"
|
||||||
@@ -720,17 +731,17 @@
|
|||||||
(tr :class "border-b border-stone-100"
|
(tr :class "border-b border-stone-100"
|
||||||
(td :class "px-3 py-2 text-stone-700" "Error boundaries")
|
(td :class "px-3 py-2 text-stone-700" "Error boundaries")
|
||||||
(td :class "px-3 py-2 text-stone-500 text-xs" "componentDidCatch")
|
(td :class "px-3 py-2 text-stone-500 text-xs" "componentDidCatch")
|
||||||
(td :class "px-3 py-2 text-stone-500 font-medium" "P2")
|
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
||||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx"))
|
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx"))
|
||||||
(tr :class "border-b border-stone-100"
|
(tr :class "border-b border-stone-100"
|
||||||
(td :class "px-3 py-2 text-stone-700" "Suspense")
|
(td :class "px-3 py-2 text-stone-700" "Suspense")
|
||||||
(td :class "px-3 py-2 text-stone-500 text-xs" "Suspense + lazy")
|
(td :class "px-3 py-2 text-stone-500 text-xs" "Suspense + lazy")
|
||||||
(td :class "px-3 py-2 text-stone-500 font-medium" "P2")
|
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
||||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx, signals.sx"))
|
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx, signals.sx"))
|
||||||
(tr
|
(tr
|
||||||
(td :class "px-3 py-2 text-stone-700" "Transitions")
|
(td :class "px-3 py-2 text-stone-700" "Transitions")
|
||||||
(td :class "px-3 py-2 text-stone-500 text-xs" "startTransition")
|
(td :class "px-3 py-2 text-stone-500 text-xs" "startTransition")
|
||||||
(td :class "px-3 py-2 text-stone-500 font-medium" "P2")
|
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
||||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "signals.sx"))))))
|
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "signals.sx"))))))
|
||||||
|
|
||||||
;; -----------------------------------------------------------------------
|
;; -----------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user