Bootstrap stores + event bridge, add island hydration to boot.sx
- signals.sx: fix has? → has-key?, add def-store/use-store/clear-stores (L3 named stores), emit-event/on-event/bridge-event (event bridge) - boot.sx: add sx-hydrate-islands, hydrate-island, dispose-island for client-side island hydration from SSR output - bootstrap_js.py: add RENAMES, platform fns (domListen, eventDetail, domGetData, jsonParse), public API exports for all new functions - bootstrap_py.py: add RENAMES, server-side no-op stubs for DOM events - Regenerate sx-ref.js (with boot adapter) and sx_ref.py - Update reactive-islands status: hydration, stores, bridge all spec'd 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 SX_VERSION = "2026-03-08T10:13:40Z";
|
||||
var SX_VERSION = "2026-03-08T11:12:31Z";
|
||||
|
||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||
@@ -572,6 +572,92 @@
|
||||
return makeThunk(componentBody(comp), local);
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
// Platform: deps module — component dependency analysis
|
||||
// =========================================================================
|
||||
|
||||
function componentDeps(c) {
|
||||
return c.deps ? c.deps.slice() : [];
|
||||
}
|
||||
|
||||
function componentSetDeps(c, deps) {
|
||||
c.deps = deps;
|
||||
}
|
||||
|
||||
function componentCssClasses(c) {
|
||||
return c.cssClasses ? c.cssClasses.slice() : [];
|
||||
}
|
||||
|
||||
function envComponents(env) {
|
||||
var names = [];
|
||||
for (var k in env) {
|
||||
var v = env[k];
|
||||
if (v && (v._component || v._macro)) names.push(k);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
function regexFindAll(pattern, source) {
|
||||
var re = new RegExp(pattern, "g");
|
||||
var results = [];
|
||||
var m;
|
||||
while ((m = re.exec(source)) !== null) {
|
||||
if (m[1] !== undefined) results.push(m[1]);
|
||||
else results.push(m[0]);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function scanCssClasses(source) {
|
||||
var classes = {};
|
||||
var result = [];
|
||||
var m;
|
||||
var re1 = /:class\s+"([^"]*)"/g;
|
||||
while ((m = re1.exec(source)) !== null) {
|
||||
var parts = m[1].split(/\s+/);
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
if (parts[i] && !classes[parts[i]]) {
|
||||
classes[parts[i]] = true;
|
||||
result.push(parts[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
var re2 = /:class\s+\(str\s+((?:"[^"]*"\s*)+)\)/g;
|
||||
while ((m = re2.exec(source)) !== null) {
|
||||
var re3 = /"([^"]*)"/g;
|
||||
var m2;
|
||||
while ((m2 = re3.exec(m[1])) !== null) {
|
||||
var parts2 = m2[1].split(/\s+/);
|
||||
for (var j = 0; j < parts2.length; j++) {
|
||||
if (parts2[j] && !classes[parts2[j]]) {
|
||||
classes[parts2[j]] = true;
|
||||
result.push(parts2[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var re4 = /;;\s*@css\s+(.+)/g;
|
||||
while ((m = re4.exec(source)) !== null) {
|
||||
var parts3 = m[1].split(/\s+/);
|
||||
for (var k = 0; k < parts3.length; k++) {
|
||||
if (parts3[k] && !classes[parts3[k]]) {
|
||||
classes[parts3[k]] = true;
|
||||
result.push(parts3[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function componentIoRefs(c) {
|
||||
return c.ioRefs ? c.ioRefs.slice() : [];
|
||||
}
|
||||
|
||||
function componentSetIoRefs(c, refs) {
|
||||
c.ioRefs = refs;
|
||||
}
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// Platform interface — Parser
|
||||
// =========================================================================
|
||||
@@ -2179,6 +2265,93 @@ return (function() {
|
||||
})() : NIL);
|
||||
})(); };
|
||||
|
||||
// _optimistic-snapshots
|
||||
var _optimisticSnapshots = {};
|
||||
|
||||
// optimistic-cache-update
|
||||
var optimisticCacheUpdate = function(cacheKey, mutator) { return (function() {
|
||||
var cached = pageDataCacheGet(cacheKey);
|
||||
return (isSxTruthy(cached) ? (function() {
|
||||
var predicted = mutator(cached);
|
||||
_optimisticSnapshots[cacheKey] = cached;
|
||||
pageDataCacheSet(cacheKey, predicted);
|
||||
return predicted;
|
||||
})() : NIL);
|
||||
})(); };
|
||||
|
||||
// optimistic-cache-revert
|
||||
var optimisticCacheRevert = function(cacheKey) { return (function() {
|
||||
var snapshot = get(_optimisticSnapshots, cacheKey);
|
||||
return (isSxTruthy(snapshot) ? (pageDataCacheSet(cacheKey, snapshot), dictDelete(_optimisticSnapshots, cacheKey), snapshot) : NIL);
|
||||
})(); };
|
||||
|
||||
// optimistic-cache-confirm
|
||||
var optimisticCacheConfirm = function(cacheKey) { return dictDelete(_optimisticSnapshots, cacheKey); };
|
||||
|
||||
// submit-mutation
|
||||
var submitMutation = function(pageName, params, actionName, payload, mutatorFn, onComplete) { return (function() {
|
||||
var cacheKey = pageDataCacheKey(pageName, params);
|
||||
var predicted = optimisticCacheUpdate(cacheKey, mutatorFn);
|
||||
if (isSxTruthy(predicted)) {
|
||||
tryRerenderPage(pageName, params, predicted);
|
||||
}
|
||||
return executeAction(actionName, payload, function(result) { if (isSxTruthy(result)) {
|
||||
pageDataCacheSet(cacheKey, result);
|
||||
}
|
||||
optimisticCacheConfirm(cacheKey);
|
||||
if (isSxTruthy(result)) {
|
||||
tryRerenderPage(pageName, params, result);
|
||||
}
|
||||
logInfo((String("sx:optimistic confirmed ") + String(pageName)));
|
||||
return (isSxTruthy(onComplete) ? onComplete("confirmed") : NIL); }, function(error) { return (function() {
|
||||
var reverted = optimisticCacheRevert(cacheKey);
|
||||
if (isSxTruthy(reverted)) {
|
||||
tryRerenderPage(pageName, params, reverted);
|
||||
}
|
||||
logWarn((String("sx:optimistic reverted ") + String(pageName) + String(": ") + String(error)));
|
||||
return (isSxTruthy(onComplete) ? onComplete("reverted") : NIL);
|
||||
})(); });
|
||||
})(); };
|
||||
|
||||
// _is-online
|
||||
var _isOnline = true;
|
||||
|
||||
// _offline-queue
|
||||
var _offlineQueue = [];
|
||||
|
||||
// offline-is-online?
|
||||
var offlineIsOnline_p = function() { return _isOnline; };
|
||||
|
||||
// offline-set-online!
|
||||
var offlineSetOnline_b = function(val) { return (_isOnline = val); };
|
||||
|
||||
// offline-queue-mutation
|
||||
var offlineQueueMutation = function(actionName, payload, pageName, params, mutatorFn) { return (function() {
|
||||
var cacheKey = pageDataCacheKey(pageName, params);
|
||||
var entry = {["action"]: actionName, ["payload"]: payload, ["page"]: pageName, ["params"]: params, ["timestamp"]: nowMs(), ["status"]: "pending"};
|
||||
_offlineQueue.push(entry);
|
||||
(function() {
|
||||
var predicted = optimisticCacheUpdate(cacheKey, mutatorFn);
|
||||
return (isSxTruthy(predicted) ? tryRerenderPage(pageName, params, predicted) : NIL);
|
||||
})();
|
||||
logInfo((String("sx:offline queued ") + String(actionName) + String(" (") + String(len(_offlineQueue)) + String(" pending)")));
|
||||
return entry;
|
||||
})(); };
|
||||
|
||||
// offline-sync
|
||||
var offlineSync = function() { return (function() {
|
||||
var pending = filter(function(e) { return (get(e, "status") == "pending"); }, _offlineQueue);
|
||||
return (isSxTruthy(!isSxTruthy(isEmpty(pending))) ? (logInfo((String("sx:offline syncing ") + String(len(pending)) + String(" mutations"))), forEach(function(entry) { return executeAction(get(entry, "action"), get(entry, "payload"), function(result) { entry["status"] = "synced";
|
||||
return logInfo((String("sx:offline synced ") + String(get(entry, "action")))); }, function(error) { entry["status"] = "failed";
|
||||
return logWarn((String("sx:offline sync failed ") + String(get(entry, "action")) + String(": ") + String(error))); }); }, pending)) : NIL);
|
||||
})(); };
|
||||
|
||||
// offline-pending-count
|
||||
var offlinePendingCount = function() { return len(filter(function(e) { return (get(e, "status") == "pending"); }, _offlineQueue)); };
|
||||
|
||||
// offline-aware-mutation
|
||||
var offlineAwareMutation = function(pageName, params, actionName, payload, mutatorFn, onComplete) { return (isSxTruthy(_isOnline) ? submitMutation(pageName, params, actionName, payload, mutatorFn, onComplete) : (offlineQueueMutation(actionName, payload, pageName, params, mutatorFn), (isSxTruthy(onComplete) ? onComplete("queued") : NIL))); };
|
||||
|
||||
// current-page-layout
|
||||
var currentPageLayout = function() { return (function() {
|
||||
var pathname = urlPathname(browserLocationHref());
|
||||
@@ -2382,7 +2555,8 @@ return bindInlineHandlers(root); };
|
||||
domAppend(el, node);
|
||||
hoistHeadElementsFull(el);
|
||||
processElements(el);
|
||||
return sxHydrateElements(el);
|
||||
sxHydrateElements(el);
|
||||
return sxHydrateIslands(el);
|
||||
})() : NIL);
|
||||
})(); };
|
||||
|
||||
@@ -2397,6 +2571,7 @@ return (function() {
|
||||
{ var _c = exprs; for (var _i = 0; _i < _c.length; _i++) { var expr = _c[_i]; domAppend(el, renderToDom(expr, env, NIL)); } }
|
||||
processElements(el);
|
||||
sxHydrateElements(el);
|
||||
sxHydrateIslands(el);
|
||||
return domDispatch(el, "sx:resolved", {"id": id});
|
||||
})() : logWarn((String("resolveSuspense: no element for id=") + String(id))));
|
||||
})(); };
|
||||
@@ -2491,8 +2666,186 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
return logInfo((String("pages: ") + String(len(_pageRoutes)) + String(" routes loaded")));
|
||||
})(); };
|
||||
|
||||
// sx-hydrate-islands
|
||||
var sxHydrateIslands = function(root) { return (function() {
|
||||
var els = domQueryAll(sxOr(root, domBody()), "[data-sx-island]");
|
||||
return forEach(function(el) { return (isSxTruthy(!isSxTruthy(isProcessed(el, "island-hydrated"))) ? (markProcessed(el, "island-hydrated"), hydrateIsland(el)) : NIL); }, els);
|
||||
})(); };
|
||||
|
||||
// hydrate-island
|
||||
var hydrateIsland = function(el) { return (function() {
|
||||
var name = domGetAttr(el, "data-sx-island");
|
||||
var stateJson = sxOr(domGetAttr(el, "data-sx-state"), "{}");
|
||||
return (function() {
|
||||
var compName = (String("~") + String(name));
|
||||
var env = getRenderEnv(NIL);
|
||||
return (function() {
|
||||
var comp = envGet(env, compName);
|
||||
return (isSxTruthy(!isSxTruthy(sxOr(isComponent(comp), isIsland(comp)))) ? logWarn((String("hydrate-island: unknown island ") + String(compName))) : (function() {
|
||||
var kwargs = jsonParse(stateJson);
|
||||
var disposers = [];
|
||||
var local = envMerge(componentClosure(comp), env);
|
||||
{ var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } }
|
||||
return (function() {
|
||||
var bodyDom = withIslandScope(function(disposable) { return append_b(disposers, disposable); }, function() { return renderToDom(componentBody(comp), local, NIL); });
|
||||
morphChildren(el, bodyDom);
|
||||
domSetData(el, "sx-disposers", disposers);
|
||||
processElements(el);
|
||||
return logInfo((String("hydrated island: ") + String(compName) + String(" (") + String(len(disposers)) + String(" disposers)")));
|
||||
})();
|
||||
})());
|
||||
})();
|
||||
})();
|
||||
})(); };
|
||||
|
||||
// dispose-island
|
||||
var disposeIsland = function(el) { return (function() {
|
||||
var disposers = domGetData(el, "sx-disposers");
|
||||
return (isSxTruthy(disposers) ? (forEach(function(d) { return (isSxTruthy(isCallable(d)) ? d() : NIL); }, disposers), domSetData(el, "sx-disposers", NIL)) : NIL);
|
||||
})(); };
|
||||
|
||||
// boot-init
|
||||
var bootInit = function() { return (logInfo((String("sx-browser ") + String(SX_VERSION))), initCssTracking(), processPageScripts(), processSxScripts(NIL), sxHydrateElements(NIL), processElements(NIL)); };
|
||||
var bootInit = function() { return (logInfo((String("sx-browser ") + String(SX_VERSION))), initCssTracking(), processPageScripts(), processSxScripts(NIL), sxHydrateElements(NIL), sxHydrateIslands(NIL), processElements(NIL)); };
|
||||
|
||||
|
||||
// === Transpiled from deps (component dependency analysis) ===
|
||||
|
||||
// scan-refs
|
||||
var scanRefs = function(node) { return (function() {
|
||||
var refs = [];
|
||||
scanRefsWalk(node, refs);
|
||||
return refs;
|
||||
})(); };
|
||||
|
||||
// scan-refs-walk
|
||||
var scanRefsWalk = function(node, refs) { return (isSxTruthy((typeOf(node) == "symbol")) ? (function() {
|
||||
var name = symbolName(node);
|
||||
return (isSxTruthy(startsWith(name, "~")) ? (isSxTruthy(!isSxTruthy(contains(refs, name))) ? append_b(refs, name) : NIL) : NIL);
|
||||
})() : (isSxTruthy((typeOf(node) == "list")) ? forEach(function(item) { return scanRefsWalk(item, refs); }, node) : (isSxTruthy((typeOf(node) == "dict")) ? forEach(function(key) { return scanRefsWalk(dictGet(node, key), refs); }, keys(node)) : NIL))); };
|
||||
|
||||
// transitive-deps-walk
|
||||
var transitiveDepsWalk = function(n, seen, env) { return (isSxTruthy(!isSxTruthy(contains(seen, n))) ? (append_b(seen, n), (function() {
|
||||
var val = envGet(env, n);
|
||||
return (isSxTruthy((typeOf(val) == "component")) ? forEach(function(ref) { return transitiveDepsWalk(ref, seen, env); }, scanRefs(componentBody(val))) : (isSxTruthy((typeOf(val) == "macro")) ? forEach(function(ref) { return transitiveDepsWalk(ref, seen, env); }, scanRefs(macroBody(val))) : NIL));
|
||||
})()) : NIL); };
|
||||
|
||||
// transitive-deps
|
||||
var transitiveDeps = function(name, env) { return (function() {
|
||||
var seen = [];
|
||||
var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name)));
|
||||
transitiveDepsWalk(key, seen, env);
|
||||
return filter(function(x) { return !isSxTruthy((x == key)); }, seen);
|
||||
})(); };
|
||||
|
||||
// compute-all-deps
|
||||
var computeAllDeps = function(env) { return forEach(function(name) { return (function() {
|
||||
var val = envGet(env, name);
|
||||
return (isSxTruthy((typeOf(val) == "component")) ? componentSetDeps(val, transitiveDeps(name, env)) : NIL);
|
||||
})(); }, envComponents(env)); };
|
||||
|
||||
// scan-components-from-source
|
||||
var scanComponentsFromSource = function(source) { return (function() {
|
||||
var matches = regexFindAll("\\(~([a-zA-Z_][a-zA-Z0-9_\\-]*)", source);
|
||||
return map(function(m) { return (String("~") + String(m)); }, matches);
|
||||
})(); };
|
||||
|
||||
// components-needed
|
||||
var componentsNeeded = function(pageSource, env) { return (function() {
|
||||
var direct = scanComponentsFromSource(pageSource);
|
||||
var allNeeded = [];
|
||||
{ var _c = direct; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; if (isSxTruthy(!isSxTruthy(contains(allNeeded, name)))) {
|
||||
allNeeded.push(name);
|
||||
}
|
||||
(function() {
|
||||
var val = envGet(env, name);
|
||||
return (function() {
|
||||
var deps = (isSxTruthy((isSxTruthy((typeOf(val) == "component")) && !isSxTruthy(isEmpty(componentDeps(val))))) ? componentDeps(val) : transitiveDeps(name, env));
|
||||
return forEach(function(dep) { return (isSxTruthy(!isSxTruthy(contains(allNeeded, dep))) ? append_b(allNeeded, dep) : NIL); }, deps);
|
||||
})();
|
||||
})(); } }
|
||||
return allNeeded;
|
||||
})(); };
|
||||
|
||||
// page-component-bundle
|
||||
var pageComponentBundle = function(pageSource, env) { return componentsNeeded(pageSource, env); };
|
||||
|
||||
// page-css-classes
|
||||
var pageCssClasses = function(pageSource, env) { return (function() {
|
||||
var needed = componentsNeeded(pageSource, env);
|
||||
var classes = [];
|
||||
{ var _c = needed; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; (function() {
|
||||
var val = envGet(env, name);
|
||||
return (isSxTruthy((typeOf(val) == "component")) ? forEach(function(cls) { return (isSxTruthy(!isSxTruthy(contains(classes, cls))) ? append_b(classes, cls) : NIL); }, componentCssClasses(val)) : NIL);
|
||||
})(); } }
|
||||
{ var _c = scanCssClasses(pageSource); for (var _i = 0; _i < _c.length; _i++) { var cls = _c[_i]; if (isSxTruthy(!isSxTruthy(contains(classes, cls)))) {
|
||||
classes.push(cls);
|
||||
} } }
|
||||
return classes;
|
||||
})(); };
|
||||
|
||||
// scan-io-refs-walk
|
||||
var scanIoRefsWalk = function(node, ioNames, refs) { return (isSxTruthy((typeOf(node) == "symbol")) ? (function() {
|
||||
var name = symbolName(node);
|
||||
return (isSxTruthy(contains(ioNames, name)) ? (isSxTruthy(!isSxTruthy(contains(refs, name))) ? append_b(refs, name) : NIL) : NIL);
|
||||
})() : (isSxTruthy((typeOf(node) == "list")) ? forEach(function(item) { return scanIoRefsWalk(item, ioNames, refs); }, node) : (isSxTruthy((typeOf(node) == "dict")) ? forEach(function(key) { return scanIoRefsWalk(dictGet(node, key), ioNames, refs); }, keys(node)) : NIL))); };
|
||||
|
||||
// scan-io-refs
|
||||
var scanIoRefs = function(node, ioNames) { return (function() {
|
||||
var refs = [];
|
||||
scanIoRefsWalk(node, ioNames, refs);
|
||||
return refs;
|
||||
})(); };
|
||||
|
||||
// transitive-io-refs-walk
|
||||
var transitiveIoRefsWalk = function(n, seen, allRefs, env, ioNames) { return (isSxTruthy(!isSxTruthy(contains(seen, n))) ? (append_b(seen, n), (function() {
|
||||
var val = envGet(env, n);
|
||||
return (isSxTruthy((typeOf(val) == "component")) ? (forEach(function(ref) { return (isSxTruthy(!isSxTruthy(contains(allRefs, ref))) ? append_b(allRefs, ref) : NIL); }, scanIoRefs(componentBody(val), ioNames)), forEach(function(dep) { return transitiveIoRefsWalk(dep, seen, allRefs, env, ioNames); }, scanRefs(componentBody(val)))) : (isSxTruthy((typeOf(val) == "macro")) ? (forEach(function(ref) { return (isSxTruthy(!isSxTruthy(contains(allRefs, ref))) ? append_b(allRefs, ref) : NIL); }, scanIoRefs(macroBody(val), ioNames)), forEach(function(dep) { return transitiveIoRefsWalk(dep, seen, allRefs, env, ioNames); }, scanRefs(macroBody(val)))) : NIL));
|
||||
})()) : NIL); };
|
||||
|
||||
// transitive-io-refs
|
||||
var transitiveIoRefs = function(name, env, ioNames) { return (function() {
|
||||
var allRefs = [];
|
||||
var seen = [];
|
||||
var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name)));
|
||||
transitiveIoRefsWalk(key, seen, allRefs, env, ioNames);
|
||||
return allRefs;
|
||||
})(); };
|
||||
|
||||
// compute-all-io-refs
|
||||
var computeAllIoRefs = function(env, ioNames) { return forEach(function(name) { return (function() {
|
||||
var val = envGet(env, name);
|
||||
return (isSxTruthy((typeOf(val) == "component")) ? componentSetIoRefs(val, transitiveIoRefs(name, env, ioNames)) : NIL);
|
||||
})(); }, envComponents(env)); };
|
||||
|
||||
// component-pure?
|
||||
var componentPure_p = function(name, env, ioNames) { return isEmpty(transitiveIoRefs(name, env, ioNames)); };
|
||||
|
||||
// render-target
|
||||
var renderTarget = function(name, env, ioNames) { return (function() {
|
||||
var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name)));
|
||||
return (function() {
|
||||
var val = envGet(env, key);
|
||||
return (isSxTruthy(!isSxTruthy((typeOf(val) == "component"))) ? "server" : (function() {
|
||||
var affinity = componentAffinity(val);
|
||||
return (isSxTruthy((affinity == "server")) ? "server" : (isSxTruthy((affinity == "client")) ? "client" : (isSxTruthy(!isSxTruthy(componentPure_p(name, env, ioNames))) ? "server" : "client")));
|
||||
})());
|
||||
})();
|
||||
})(); };
|
||||
|
||||
// page-render-plan
|
||||
var pageRenderPlan = function(pageSource, env, ioNames) { return (function() {
|
||||
var needed = componentsNeeded(pageSource, env);
|
||||
var compTargets = {};
|
||||
var serverList = [];
|
||||
var clientList = [];
|
||||
var ioDeps = [];
|
||||
{ var _c = needed; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; (function() {
|
||||
var target = renderTarget(name, env, ioNames);
|
||||
compTargets[name] = target;
|
||||
return (isSxTruthy((target == "server")) ? (append_b(serverList, name), forEach(function(ioRef) { return (isSxTruthy(!isSxTruthy(contains(ioDeps, ioRef))) ? append_b(ioDeps, ioRef) : NIL); }, transitiveIoRefs(name, env, ioNames))) : append_b(clientList, name));
|
||||
})(); } }
|
||||
return {"components": compTargets, "server": serverList, "client": clientList, "io-deps": ioDeps};
|
||||
})(); };
|
||||
|
||||
|
||||
// === Transpiled from router (client-side route matching) ===
|
||||
@@ -2702,6 +3055,40 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
||||
// register-in-scope
|
||||
var registerInScope = function(disposable) { return (isSxTruthy(_islandScope) ? _islandScope(disposable) : NIL); };
|
||||
|
||||
// *store-registry*
|
||||
var _storeRegistry = {};
|
||||
|
||||
// def-store
|
||||
var defStore = function(name, initFn) { return (function() {
|
||||
var registry = _storeRegistry;
|
||||
if (isSxTruthy(!isSxTruthy(hasKey_p(registry, name)))) {
|
||||
_storeRegistry = assoc(registry, name, initFn());
|
||||
}
|
||||
return get(_storeRegistry, name);
|
||||
})(); };
|
||||
|
||||
// use-store
|
||||
var useStore = function(name) { return (isSxTruthy(hasKey_p(_storeRegistry, name)) ? get(_storeRegistry, name) : error((String("Store not found: ") + String(name) + String(". Call (def-store ...) before (use-store ...).")))); };
|
||||
|
||||
// clear-stores
|
||||
var clearStores = function() { return (_storeRegistry = {}); };
|
||||
|
||||
// emit-event
|
||||
var emitEvent = function(el, eventName, detail) { return domDispatch(el, eventName, detail); };
|
||||
|
||||
// on-event
|
||||
var onEvent = function(el, eventName, handler) { return domListen(el, eventName, handler); };
|
||||
|
||||
// bridge-event
|
||||
var bridgeEvent = function(el, eventName, targetSignal, transformFn) { return effect(function() { return (function() {
|
||||
var remove = domListen(el, eventName, function(e) { return (function() {
|
||||
var detail = eventDetail(e);
|
||||
var newVal = (isSxTruthy(transformFn) ? transformFn(detail) : detail);
|
||||
return reset_b(targetSignal, newVal);
|
||||
})(); });
|
||||
return remove;
|
||||
})(); }); };
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// Platform interface — DOM adapter (browser-only)
|
||||
@@ -2852,6 +3239,16 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
||||
return el.dispatchEvent(evt);
|
||||
}
|
||||
|
||||
function domListen(el, name, handler) {
|
||||
if (!_hasDom || !el) return function() {};
|
||||
el.addEventListener(name, handler);
|
||||
return function() { el.removeEventListener(name, handler); };
|
||||
}
|
||||
|
||||
function eventDetail(e) {
|
||||
return (e && e.detail != null) ? e.detail : nil;
|
||||
}
|
||||
|
||||
function domQuery(sel) {
|
||||
return _hasDom ? document.querySelector(sel) : null;
|
||||
}
|
||||
@@ -2879,6 +3276,12 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
||||
function domSetData(el, key, val) {
|
||||
if (el) { if (!el._sxData) el._sxData = {}; el._sxData[key] = val; }
|
||||
}
|
||||
function domGetData(el, key) {
|
||||
return (el && el._sxData) ? (el._sxData[key] != null ? el._sxData[key] : nil) : nil;
|
||||
}
|
||||
function jsonParse(s) {
|
||||
try { return JSON.parse(s); } catch(e) { return {}; }
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Performance overrides — replace transpiled spec with imperative JS
|
||||
@@ -4706,7 +5109,20 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
||||
renderComponent: typeof sxRenderComponent === "function" ? sxRenderComponent : null,
|
||||
getEnv: function() { return componentEnv; },
|
||||
resolveSuspense: typeof resolveSuspense === "function" ? resolveSuspense : null,
|
||||
hydrateIslands: typeof sxHydrateIslands === "function" ? sxHydrateIslands : null,
|
||||
disposeIsland: typeof disposeIsland === "function" ? disposeIsland : null,
|
||||
init: typeof bootInit === "function" ? bootInit : null,
|
||||
scanRefs: scanRefs,
|
||||
scanComponentsFromSource: scanComponentsFromSource,
|
||||
transitiveDeps: transitiveDeps,
|
||||
computeAllDeps: computeAllDeps,
|
||||
componentsNeeded: componentsNeeded,
|
||||
pageComponentBundle: pageComponentBundle,
|
||||
pageCssClasses: pageCssClasses,
|
||||
scanIoRefs: scanIoRefs,
|
||||
transitiveIoRefs: transitiveIoRefs,
|
||||
computeAllIoRefs: computeAllIoRefs,
|
||||
componentPure_p: componentPure_p,
|
||||
splitPathSegments: splitPathSegments,
|
||||
parseRoutePattern: parseRoutePattern,
|
||||
matchRoute: matchRoute,
|
||||
@@ -4724,6 +5140,12 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
||||
batch: batch,
|
||||
isSignal: isSignal,
|
||||
makeSignal: makeSignal,
|
||||
defStore: defStore,
|
||||
useStore: useStore,
|
||||
clearStores: clearStores,
|
||||
emitEvent: emitEvent,
|
||||
onEvent: onEvent,
|
||||
bridgeEvent: bridgeEvent,
|
||||
_version: "ref-2.0 (boot+dom+engine+html+orchestration+parser+sx, bootstrap-compiled)"
|
||||
};
|
||||
|
||||
@@ -4766,4 +5188,4 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
||||
if (typeof module !== "undefined" && module.exports) module.exports = Sx;
|
||||
else global.Sx = Sx;
|
||||
|
||||
})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this);
|
||||
})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this);
|
||||
@@ -84,9 +84,10 @@
|
||||
(dom-append el node)
|
||||
;; Hoist head elements from rendered content
|
||||
(hoist-head-elements-full el)
|
||||
;; Process sx- attributes and hydrate
|
||||
;; Process sx- attributes, hydrate data-sx and islands
|
||||
(process-elements el)
|
||||
(sx-hydrate-elements el))))))
|
||||
(sx-hydrate-elements el)
|
||||
(sx-hydrate-islands el))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -117,6 +118,7 @@
|
||||
exprs)
|
||||
(process-elements el)
|
||||
(sx-hydrate-elements el)
|
||||
(sx-hydrate-islands el)
|
||||
(dom-dispatch el "sx:resolved" {:id id})))
|
||||
(log-warn (str "resolveSuspense: no element for id=" id))))))
|
||||
|
||||
@@ -305,6 +307,88 @@
|
||||
(log-info (str "pages: " (len _page-routes) " routes loaded")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Island hydration — activate reactive islands from SSR output
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; The server renders islands as:
|
||||
;; <div data-sx-island="counter" data-sx-state='{"initial": 0}'>
|
||||
;; ...static HTML...
|
||||
;; </div>
|
||||
;;
|
||||
;; Hydration:
|
||||
;; 1. Find all [data-sx-island] elements
|
||||
;; 2. Look up the island component by name
|
||||
;; 3. Parse data-sx-state into kwargs
|
||||
;; 4. Re-render the island body in a reactive context
|
||||
;; 5. Morph existing DOM to preserve structure, focus, scroll
|
||||
;; 6. Store disposers on the element for cleanup
|
||||
|
||||
(define sx-hydrate-islands
|
||||
(fn (root)
|
||||
(let ((els (dom-query-all (or root (dom-body)) "[data-sx-island]")))
|
||||
(for-each
|
||||
(fn (el)
|
||||
(when (not (is-processed? el "island-hydrated"))
|
||||
(mark-processed! el "island-hydrated")
|
||||
(hydrate-island el)))
|
||||
els))))
|
||||
|
||||
(define hydrate-island
|
||||
(fn (el)
|
||||
(let ((name (dom-get-attr el "data-sx-island"))
|
||||
(state-json (or (dom-get-attr el "data-sx-state") "{}")))
|
||||
(let ((comp-name (str "~" name))
|
||||
(env (get-render-env nil)))
|
||||
(let ((comp (env-get env comp-name)))
|
||||
(if (not (or (component? comp) (island? comp)))
|
||||
(log-warn (str "hydrate-island: unknown island " comp-name))
|
||||
|
||||
;; Parse state and build keyword args
|
||||
(let ((kwargs (json-parse state-json))
|
||||
(disposers (list))
|
||||
(local (env-merge (component-closure comp) env)))
|
||||
|
||||
;; Bind params from kwargs
|
||||
(for-each
|
||||
(fn (p)
|
||||
(env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(component-params comp))
|
||||
|
||||
;; Render the island body in a reactive scope
|
||||
(let ((body-dom
|
||||
(with-island-scope
|
||||
(fn (disposable) (append! disposers disposable))
|
||||
(fn () (render-to-dom (component-body comp) local nil)))))
|
||||
|
||||
;; Morph existing DOM against reactive output
|
||||
(morph-children el body-dom)
|
||||
|
||||
;; Store disposers for cleanup
|
||||
(dom-set-data el "sx-disposers" disposers)
|
||||
|
||||
;; Process any sx- attributes on new content
|
||||
(process-elements el)
|
||||
|
||||
(log-info (str "hydrated island: " comp-name
|
||||
" (" (len disposers) " disposers)"))))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Island disposal — clean up when island removed from DOM
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define dispose-island
|
||||
(fn (el)
|
||||
(let ((disposers (dom-get-data el "sx-disposers")))
|
||||
(when disposers
|
||||
(for-each
|
||||
(fn (d)
|
||||
(when (callable? d) (d)))
|
||||
disposers)
|
||||
(dom-set-data el "sx-disposers" nil)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Full boot sequence
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -317,13 +401,15 @@
|
||||
;; 3. Process scripts (components + mounts)
|
||||
;; 4. Process page registry (client-side routing)
|
||||
;; 5. Hydrate [data-sx] elements
|
||||
;; 6. Process engine elements
|
||||
;; 6. Hydrate [data-sx-island] elements (reactive islands)
|
||||
;; 7. Process engine elements
|
||||
(do
|
||||
(log-info (str "sx-browser " SX_VERSION))
|
||||
(init-css-tracking)
|
||||
(process-page-scripts)
|
||||
(process-sx-scripts nil)
|
||||
(sx-hydrate-elements nil)
|
||||
(sx-hydrate-islands nil)
|
||||
(process-elements nil))))
|
||||
|
||||
|
||||
@@ -382,8 +468,25 @@
|
||||
;; (log-info msg) → void (console.log with prefix)
|
||||
;; (log-parse-error label text err) → void (diagnostic parse error)
|
||||
;;
|
||||
;; === JSON parsing ===
|
||||
;; === JSON ===
|
||||
;; (json-parse str) → dict/list/value (JSON.parse)
|
||||
;;
|
||||
;; === Processing markers ===
|
||||
;; (mark-processed! el key) → void
|
||||
;; (is-processed? el key) → boolean
|
||||
;;
|
||||
;; === Morph ===
|
||||
;; (morph-children target source) → void (morph target's children to match source)
|
||||
;;
|
||||
;; === Island support (from adapter-dom.sx / signals.sx) ===
|
||||
;; (island? x) → boolean
|
||||
;; (component-closure comp) → env
|
||||
;; (component-params comp) → list of param names
|
||||
;; (component-body comp) → AST
|
||||
;; (component-name comp) → string
|
||||
;; (component-has-children? comp) → boolean
|
||||
;; (with-island-scope scope-fn body-fn) → result (track disposables)
|
||||
;; (render-to-dom expr env ns) → DOM node
|
||||
;; (dom-get-data el key) → any (from el._sxData)
|
||||
;; (dom-set-data el key val) → void
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
@@ -165,6 +165,13 @@ class JSEmitter:
|
||||
"*batch-depth*": "_batchDepth",
|
||||
"*batch-queue*": "_batchQueue",
|
||||
"*island-scope*": "_islandScope",
|
||||
"*store-registry*": "_storeRegistry",
|
||||
"def-store": "defStore",
|
||||
"use-store": "useStore",
|
||||
"clear-stores": "clearStores",
|
||||
"emit-event": "emitEvent",
|
||||
"on-event": "onEvent",
|
||||
"bridge-event": "bridgeEvent",
|
||||
"macro?": "isMacro",
|
||||
"primitive?": "isPrimitive",
|
||||
"get-primitive": "getPrimitive",
|
||||
@@ -314,6 +321,8 @@ class JSEmitter:
|
||||
"dom-add-class": "domAddClass",
|
||||
"dom-remove-class": "domRemoveClass",
|
||||
"dom-dispatch": "domDispatch",
|
||||
"dom-listen": "domListen",
|
||||
"event-detail": "eventDetail",
|
||||
"dom-query": "domQuery",
|
||||
"dom-query-all": "domQueryAll",
|
||||
"dom-tag-name": "domTagName",
|
||||
@@ -322,6 +331,8 @@ class JSEmitter:
|
||||
"dom-child-nodes": "domChildNodes",
|
||||
"dom-remove-children-after": "domRemoveChildrenAfter",
|
||||
"dom-set-data": "domSetData",
|
||||
"dom-get-data": "domGetData",
|
||||
"json-parse": "jsonParse",
|
||||
"dict-has?": "dictHas",
|
||||
"dict-delete!": "dictDelete",
|
||||
"process-bindings": "processBindings",
|
||||
@@ -508,6 +519,9 @@ class JSEmitter:
|
||||
"process-component-script": "processComponentScript",
|
||||
"SX_VERSION": "SX_VERSION",
|
||||
"boot-init": "bootInit",
|
||||
"sx-hydrate-islands": "sxHydrateIslands",
|
||||
"hydrate-island": "hydrateIsland",
|
||||
"dispose-island": "disposeIsland",
|
||||
"resolve-suspense": "resolveSuspense",
|
||||
"resolve-mount-target": "resolveMountTarget",
|
||||
"sx-render-with-env": "sxRenderWithEnv",
|
||||
@@ -2870,6 +2884,16 @@ PLATFORM_DOM_JS = """
|
||||
return el.dispatchEvent(evt);
|
||||
}
|
||||
|
||||
function domListen(el, name, handler) {
|
||||
if (!_hasDom || !el) return function() {};
|
||||
el.addEventListener(name, handler);
|
||||
return function() { el.removeEventListener(name, handler); };
|
||||
}
|
||||
|
||||
function eventDetail(e) {
|
||||
return (e && e.detail != null) ? e.detail : nil;
|
||||
}
|
||||
|
||||
function domQuery(sel) {
|
||||
return _hasDom ? document.querySelector(sel) : null;
|
||||
}
|
||||
@@ -2897,6 +2921,12 @@ PLATFORM_DOM_JS = """
|
||||
function domSetData(el, key, val) {
|
||||
if (el) { if (!el._sxData) el._sxData = {}; el._sxData[key] = val; }
|
||||
}
|
||||
function domGetData(el, key) {
|
||||
return (el && el._sxData) ? (el._sxData[key] != null ? el._sxData[key] : nil) : nil;
|
||||
}
|
||||
function jsonParse(s) {
|
||||
try { return JSON.parse(s); } catch(e) { return {}; }
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Performance overrides — replace transpiled spec with imperative JS
|
||||
@@ -4142,6 +4172,8 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has
|
||||
api_lines.append(' renderComponent: typeof sxRenderComponent === "function" ? sxRenderComponent : null,')
|
||||
api_lines.append(' getEnv: function() { return componentEnv; },')
|
||||
api_lines.append(' resolveSuspense: typeof resolveSuspense === "function" ? resolveSuspense : null,')
|
||||
api_lines.append(' hydrateIslands: typeof sxHydrateIslands === "function" ? sxHydrateIslands : null,')
|
||||
api_lines.append(' disposeIsland: typeof disposeIsland === "function" ? disposeIsland : null,')
|
||||
api_lines.append(' init: typeof bootInit === "function" ? bootInit : null,')
|
||||
elif has_orch:
|
||||
api_lines.append(' init: typeof engineInit === "function" ? engineInit : null,')
|
||||
@@ -4178,6 +4210,12 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has
|
||||
api_lines.append(' batch: batch,')
|
||||
api_lines.append(' isSignal: isSignal,')
|
||||
api_lines.append(' makeSignal: makeSignal,')
|
||||
api_lines.append(' defStore: defStore,')
|
||||
api_lines.append(' useStore: useStore,')
|
||||
api_lines.append(' clearStores: clearStores,')
|
||||
api_lines.append(' emitEvent: emitEvent,')
|
||||
api_lines.append(' onEvent: onEvent,')
|
||||
api_lines.append(' bridgeEvent: bridgeEvent,')
|
||||
api_lines.append(f' _version: "{version}"')
|
||||
api_lines.append(' };')
|
||||
api_lines.append('')
|
||||
|
||||
@@ -174,6 +174,16 @@ class PyEmitter:
|
||||
"*batch-depth*": "_batch_depth",
|
||||
"*batch-queue*": "_batch_queue",
|
||||
"*island-scope*": "_island_scope",
|
||||
"*store-registry*": "_store_registry",
|
||||
"def-store": "def_store",
|
||||
"use-store": "use_store",
|
||||
"clear-stores": "clear_stores",
|
||||
"emit-event": "emit_event",
|
||||
"on-event": "on_event",
|
||||
"bridge-event": "bridge_event",
|
||||
"dom-listen": "dom_listen",
|
||||
"dom-dispatch": "dom_dispatch",
|
||||
"event-detail": "event_detail",
|
||||
"macro?": "is_macro",
|
||||
"primitive?": "is_primitive",
|
||||
"get-primitive": "get_primitive",
|
||||
@@ -1544,6 +1554,17 @@ def is_empty_dict(d):
|
||||
return len(d) == 0
|
||||
|
||||
|
||||
# DOM event primitives — no-ops on server (browser-only).
|
||||
def dom_listen(el, name, handler):
|
||||
return lambda: None
|
||||
|
||||
def dom_dispatch(el, name, detail=None):
|
||||
return False
|
||||
|
||||
def event_detail(e):
|
||||
return None
|
||||
|
||||
|
||||
def env_has(env, name):
|
||||
return name in env
|
||||
|
||||
|
||||
@@ -307,13 +307,13 @@
|
||||
(fn (name init-fn)
|
||||
(let ((registry *store-registry*))
|
||||
;; Only create the store once — subsequent calls return existing
|
||||
(when (not (has? registry name))
|
||||
(when (not (has-key? registry name))
|
||||
(set! *store-registry* (assoc registry name (init-fn))))
|
||||
(get *store-registry* name))))
|
||||
|
||||
(define use-store
|
||||
(fn (name)
|
||||
(if (has? *store-registry* name)
|
||||
(if (has-key? *store-registry* name)
|
||||
(get *store-registry* name)
|
||||
(error (str "Store not found: " name
|
||||
". Call (def-store ...) before (use-store ...).")))))
|
||||
|
||||
@@ -416,6 +416,17 @@ def is_empty_dict(d):
|
||||
return len(d) == 0
|
||||
|
||||
|
||||
# DOM event primitives — no-ops on server (browser-only).
|
||||
def dom_listen(el, name, handler):
|
||||
return lambda: None
|
||||
|
||||
def dom_dispatch(el, name, detail=None):
|
||||
return False
|
||||
|
||||
def event_detail(e):
|
||||
return None
|
||||
|
||||
|
||||
def env_has(env, name):
|
||||
return name in env
|
||||
|
||||
@@ -1346,24 +1357,6 @@ render_html_island = lambda island, args, env: (lambda kwargs: (lambda children:
|
||||
serialize_island_state = lambda kwargs: (NIL if sx_truthy(is_empty_dict(kwargs)) else json_serialize(kwargs))
|
||||
|
||||
|
||||
# === Transpiled from adapter-sx ===
|
||||
|
||||
# render-to-sx
|
||||
render_to_sx = lambda expr, env: (lambda result: (result if sx_truthy((type_of(result) == 'string')) else serialize(result)))(aser(expr, env))
|
||||
|
||||
# aser
|
||||
aser = lambda expr, env: _sx_case(type_of(expr), [('number', lambda: expr), ('string', lambda: expr), ('boolean', lambda: expr), ('nil', lambda: NIL), ('symbol', lambda: (lambda name: (env_get(env, name) if sx_truthy(env_has(env, name)) else (get_primitive(name) if sx_truthy(is_primitive(name)) else (True if sx_truthy((name == 'true')) else (False if sx_truthy((name == 'false')) else (NIL if sx_truthy((name == 'nil')) else error(sx_str('Undefined symbol: ', name))))))))(symbol_name(expr))), ('keyword', lambda: keyword_name(expr)), ('list', lambda: ([] if sx_truthy(empty_p(expr)) else aser_list(expr, env))), (None, lambda: expr)])
|
||||
|
||||
# aser-list
|
||||
aser_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: aser(x, env), expr) if sx_truthy((not sx_truthy((type_of(head) == 'symbol')))) else (lambda name: (aser_fragment(args, env) if sx_truthy((name == '<>')) else (aser_call(name, args, env) if sx_truthy(starts_with_p(name, '~')) else (aser_call(name, args, env) if sx_truthy(contains_p(HTML_TAGS, name)) else (aser_special(name, expr, env) if sx_truthy((is_special_form(name) if sx_truthy(is_special_form(name)) else is_ho_form(name))) else (aser(expand_macro(env_get(env, name), args, env), env) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (lambda f: (lambda evaled_args: (apply(f, evaled_args) if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else ((not sx_truthy(is_lambda(f))) if not sx_truthy((not sx_truthy(is_lambda(f)))) else ((not sx_truthy(is_component(f))) if not sx_truthy((not sx_truthy(is_component(f)))) else (not sx_truthy(is_island(f))))))) else (trampoline(call_lambda(f, evaled_args, env)) if sx_truthy(is_lambda(f)) else (aser_call(sx_str('~', component_name(f)), args, env) if sx_truthy(is_component(f)) else (aser_call(sx_str('~', component_name(f)), args, env) if sx_truthy(is_island(f)) else error(sx_str('Not callable: ', inspect(f))))))))(map(lambda a: trampoline(eval_expr(a, env)), args)))(trampoline(eval_expr(head, env)))))))))(symbol_name(head))))(rest(expr)))(first(expr))
|
||||
|
||||
# aser-fragment
|
||||
aser_fragment = lambda children, env: (lambda parts: ('' if sx_truthy(empty_p(parts)) else sx_str('(<> ', join(' ', map(serialize, parts)), ')')))(filter(lambda x: (not sx_truthy(is_nil(x))), map(lambda c: aser(c, env), children)))
|
||||
|
||||
# aser-call
|
||||
aser_call = lambda name, args, env: (lambda parts: _sx_begin(reduce(lambda state, arg: (lambda skip: (assoc(state, 'skip', False, 'i', (get(state, 'i') + 1)) if sx_truthy(skip) else ((lambda val: _sx_begin((_sx_begin(_sx_append(parts, sx_str(':', keyword_name(arg))), _sx_append(parts, serialize(val))) if sx_truthy((not sx_truthy(is_nil(val)))) else NIL), assoc(state, 'skip', True, 'i', (get(state, 'i') + 1))))(aser(nth(args, (get(state, 'i') + 1)), env)) if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((get(state, 'i') + 1) < len(args)))) else (lambda val: _sx_begin((_sx_append(parts, serialize(val)) if sx_truthy((not sx_truthy(is_nil(val)))) else NIL), assoc(state, 'i', (get(state, 'i') + 1))))(aser(arg, env)))))(get(state, 'skip')), {'i': 0, 'skip': False}, args), sx_str('(', join(' ', parts), ')')))([name])
|
||||
|
||||
|
||||
# === Transpiled from deps (component dependency analysis) ===
|
||||
|
||||
# scan-refs
|
||||
@@ -1493,6 +1486,32 @@ def with_island_scope(scope_fn, body_fn):
|
||||
# register-in-scope
|
||||
register_in_scope = lambda disposable: (_island_scope(disposable) if sx_truthy(_island_scope) else NIL)
|
||||
|
||||
# *store-registry*
|
||||
_store_registry = {}
|
||||
|
||||
# def-store
|
||||
def def_store(name, init_fn):
|
||||
registry = _store_registry
|
||||
if sx_truthy((not sx_truthy(has_key_p(registry, name)))):
|
||||
_store_registry = assoc(registry, name, init_fn())
|
||||
return get(_store_registry, name)
|
||||
|
||||
# use-store
|
||||
use_store = lambda name: (get(_store_registry, name) if sx_truthy(has_key_p(_store_registry, name)) else error(sx_str('Store not found: ', name, '. Call (def-store ...) before (use-store ...).')))
|
||||
|
||||
# clear-stores
|
||||
def clear_stores():
|
||||
return _sx_cell_set(_cells, '_store_registry', {})
|
||||
|
||||
# emit-event
|
||||
emit_event = lambda el, event_name, detail: dom_dispatch(el, event_name, detail)
|
||||
|
||||
# on-event
|
||||
on_event = lambda el, event_name, handler: dom_listen(el, event_name, handler)
|
||||
|
||||
# bridge-event
|
||||
bridge_event = lambda el, event_name, target_signal, transform_fn: effect(lambda : (lambda remove: remove)(dom_listen(el, event_name, lambda e: (lambda detail: (lambda new_val: reset_b(target_signal, new_val))((transform_fn(detail) if sx_truthy(transform_fn) else detail)))(event_detail(e)))))
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Fixups -- wire up render adapter dispatch
|
||||
@@ -1530,9 +1549,6 @@ def _wrap_aser_outputs():
|
||||
# Public API
|
||||
# =========================================================================
|
||||
|
||||
# Wrap aser outputs to return SxExpr
|
||||
_wrap_aser_outputs()
|
||||
|
||||
# Set HTML as default adapter
|
||||
_setup_html_adapter()
|
||||
|
||||
|
||||
@@ -111,8 +111,8 @@
|
||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "signals.sx: emit-event, on-event, bridge-event"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Client hydration")
|
||||
(td :class "px-3 py-2 text-amber-600 font-medium" "TODO")
|
||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "boot.sx"))
|
||||
(td :class "px-3 py-2 text-green-700 font-medium" "Spec'd")
|
||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "boot.sx: sx-hydrate-islands, hydrate-island, dispose-island"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Event bindings")
|
||||
(td :class "px-3 py-2 text-amber-600 font-medium" "TODO")
|
||||
|
||||
Reference in New Issue
Block a user