Level 2-3: lake morphing — server content flows through reactive islands
Lake tag (lake :id "name" children...) creates server-morphable slots within islands. During morph, the engine enters hydrated islands and updates data-sx-lake elements by ID while preserving surrounding reactive DOM (signals, effects, event listeners). Specced in .sx, bootstrapped to JS and Python: - adapter-dom.sx: render-dom-lake, reactive-attr marks data-sx-reactive-attrs - adapter-html.sx: render-html-lake SSR output - adapter-sx.sx: lake serialized in wire format - engine.sx: morph-island-children (lake-by-ID matching), sync-attrs skips reactive attributes - ~sx-header uses lakes for logo and copyright - Hegelian essay updated with lake code example Also includes: lambda nil-padding for missing args, page env ordering fix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -338,15 +338,17 @@
|
||||
(dom-has-attr? old-node "sx-ignore"))
|
||||
nil
|
||||
|
||||
;; Hydrated island → preserve reactive state across server swaps.
|
||||
;; Hydrated island → preserve reactive state, morph lakes.
|
||||
;; If old and new are the same island (by name), keep the old DOM
|
||||
;; with its live signals, effects, and event listeners intact.
|
||||
;; But recurse into data-sx-lake slots so the server can update
|
||||
;; non-reactive content within the island.
|
||||
(and (dom-has-attr? old-node "data-sx-island")
|
||||
(is-processed? old-node "island-hydrated")
|
||||
(dom-has-attr? new-node "data-sx-island")
|
||||
(= (dom-get-attr old-node "data-sx-island")
|
||||
(dom-get-attr new-node "data-sx-island")))
|
||||
nil
|
||||
(morph-island-children old-node new-node)
|
||||
|
||||
;; Different node type or tag → replace wholesale
|
||||
(or (not (= (dom-node-type old-node) (dom-node-type new-node)))
|
||||
@@ -371,19 +373,29 @@
|
||||
|
||||
(define sync-attrs
|
||||
(fn (old-el new-el)
|
||||
;; Add/update attributes from new, remove those not in new
|
||||
(for-each
|
||||
(fn (attr)
|
||||
(let ((name (first attr))
|
||||
(val (nth attr 1)))
|
||||
(when (not (= (dom-get-attr old-el name) val))
|
||||
(dom-set-attr old-el name val))))
|
||||
(dom-attr-list new-el))
|
||||
(for-each
|
||||
(fn (attr)
|
||||
(when (not (dom-has-attr? new-el (first attr)))
|
||||
(dom-remove-attr old-el (first attr))))
|
||||
(dom-attr-list old-el))))
|
||||
;; Sync attributes from new to old, but skip reactively managed attrs.
|
||||
;; data-sx-reactive-attrs="style,class" means those attrs are owned by
|
||||
;; signal effects and must not be overwritten by the morph.
|
||||
(let ((ra-str (or (dom-get-attr old-el "data-sx-reactive-attrs") ""))
|
||||
(reactive-attrs (if (empty? ra-str) (list) (split ra-str ","))))
|
||||
;; Add/update attributes from new, skip reactive ones
|
||||
(for-each
|
||||
(fn (attr)
|
||||
(let ((name (first attr))
|
||||
(val (nth attr 1)))
|
||||
(when (and (not (= (dom-get-attr old-el name) val))
|
||||
(not (contains? reactive-attrs name)))
|
||||
(dom-set-attr old-el name val))))
|
||||
(dom-attr-list new-el))
|
||||
;; Remove attributes not in new, skip reactive + marker attrs
|
||||
(for-each
|
||||
(fn (attr)
|
||||
(let ((aname (first attr)))
|
||||
(when (and (not (dom-has-attr? new-el aname))
|
||||
(not (contains? reactive-attrs aname))
|
||||
(not (= aname "data-sx-reactive-attrs")))
|
||||
(dom-remove-attr old-el aname))))
|
||||
(dom-attr-list old-el)))))
|
||||
|
||||
|
||||
(define morph-children
|
||||
@@ -445,6 +457,44 @@
|
||||
(range oi (len old-kids))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; morph-island-children — deep morph into hydrated islands via lakes
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; Level 2-3 island morphing: the server can update non-reactive content
|
||||
;; within hydrated islands by morphing data-sx-lake slots.
|
||||
;;
|
||||
;; The island's reactive DOM (signals, effects, event listeners) is preserved.
|
||||
;; Only lake slots — explicitly marked server territory — receive new content.
|
||||
;;
|
||||
;; This is the Hegelian synthesis made concrete:
|
||||
;; - Islands = client subjectivity (reactive state, preserved)
|
||||
;; - Lakes = server substance (content, morphed)
|
||||
;; - The morph = Aufhebung (cancellation/preservation/elevation of both)
|
||||
|
||||
(define morph-island-children
|
||||
(fn (old-island new-island)
|
||||
;; Find all lake slots in both old and new islands
|
||||
(let ((old-lakes (dom-query-all old-island "[data-sx-lake]"))
|
||||
(new-lakes (dom-query-all new-island "[data-sx-lake]")))
|
||||
;; Build ID→element map for new lakes
|
||||
(let ((new-lake-map (dict)))
|
||||
(for-each
|
||||
(fn (lake)
|
||||
(let ((id (dom-get-attr lake "data-sx-lake")))
|
||||
(when id (dict-set! new-lake-map id lake))))
|
||||
new-lakes)
|
||||
;; Morph each old lake from its new counterpart
|
||||
(for-each
|
||||
(fn (old-lake)
|
||||
(let ((id (dom-get-attr old-lake "data-sx-lake")))
|
||||
(let ((new-lake (dict-get new-lake-map id)))
|
||||
(when new-lake
|
||||
(sync-attrs old-lake new-lake)
|
||||
(morph-children old-lake new-lake)))))
|
||||
old-lakes)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Swap dispatch
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user