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:
2026-03-10 14:29:54 +00:00
parent d5e416e478
commit 9b9fc6b6a5
15 changed files with 351 additions and 63 deletions

View File

@@ -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
;; --------------------------------------------------------------------------