aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorHayleigh Thompson <me@hayleigh.dev>2024-05-18 09:54:24 +0100
committerHayleigh Thompson <me@hayleigh.dev>2024-05-18 09:55:02 +0100
commit6310f758a18aa0cb0d818c590c6251074f268a8d (patch)
tree8860df4218d27d4a8d136a24cb8cf247509ef692 /src
parent6d164196490e12f3d738d32a2435692fd16f689b (diff)
downloadlustre-6310f758a18aa0cb0d818c590c6251074f268a8d.tar.gz
lustre-6310f758a18aa0cb0d818c590c6251074f268a8d.zip
:bug: Fixed a bug where unordered patches would cause runtime errors.
Diffstat (limited to 'src')
-rw-r--r--src/lustre/internals/patch.gleam60
-rw-r--r--src/lustre/server_component.gleam2
2 files changed, 49 insertions, 13 deletions
diff --git a/src/lustre/internals/patch.gleam b/src/lustre/internals/patch.gleam
index 6efa63e..3e87b81 100644
--- a/src/lustre/internals/patch.gleam
+++ b/src/lustre/internals/patch.gleam
@@ -7,6 +7,7 @@ import gleam/int
import gleam/json.{type Json}
import gleam/list
import gleam/option.{type Option, None, Some}
+import gleam/order.{type Order, Eq, Gt, Lt}
import gleam/set.{type Set}
import gleam/string
import lustre/internals/constants
@@ -236,25 +237,38 @@ pub fn patch_to_json(patch: Patch(msg)) -> Json {
}
}
+import gleam/io
+
pub fn element_diff_to_json(diff: ElementDiff(msg)) -> Json {
json.preprocessed_array([
json.preprocessed_array(
list.reverse({
- use array, key, element <- dict.fold(diff.created, [])
- let json =
- json.preprocessed_array([
- json.string(key),
- vdom.element_to_json(element),
- ])
-
- [json, ..array]
+ // Gleam's dictionaries are unordered, which is a bit of a problem for
+ // our runtime patching where we assume keys come in a stable order. To
+ // make our client code as fast as possible, we do the sort here rather
+ // than on the client.
+ dict.to_list(diff.created)
+ |> list.sort(fn(x, y) { key_sort(x.0, y.0) })
+ |> list.fold([], fn(array, patch) {
+ let #(key, element) = patch
+ io.debug(key)
+ let json =
+ json.preprocessed_array([
+ json.string(key),
+ vdom.element_to_json(element),
+ ])
+
+ [json, ..array]
+ })
}),
),
json.preprocessed_array({
- use array, key <- set.fold(diff.removed, [])
- let json = json.preprocessed_array([json.string(key)])
-
- [json, ..array]
+ set.to_list(diff.removed)
+ |> list.sort(key_sort)
+ |> list.fold([], fn(array, key) {
+ let json = json.preprocessed_array([json.string(key)])
+ [json, ..array]
+ })
}),
json.preprocessed_array(
list.reverse({
@@ -273,6 +287,28 @@ pub fn element_diff_to_json(diff: ElementDiff(msg)) -> Json {
])
}
+fn key_sort(x: String, y: String) -> Order {
+ do_key_sort(string.split(x, "-"), string.split(y, "-"))
+}
+
+fn do_key_sort(xs: List(String), ys: List(String)) -> Order {
+ case xs, ys {
+ [], [] -> Eq
+ [], _ -> Lt
+ _, [] -> Gt
+ ["-", ..xs], ["-", ..ys] -> do_key_sort(xs, ys)
+ [x, ..xs], [y, ..ys] -> {
+ let assert Ok(x) = int.parse(x)
+ let assert Ok(y) = int.parse(y)
+
+ case int.compare(x, y) {
+ Eq -> do_key_sort(xs, ys)
+ order -> order
+ }
+ }
+ }
+}
+
pub fn attribute_diff_to_json(diff: AttributeDiff(msg), key: String) -> Json {
json.preprocessed_array([
json.preprocessed_array({
diff --git a/src/lustre/server_component.gleam b/src/lustre/server_component.gleam
index 2b15314..6921e8f 100644
--- a/src/lustre/server_component.gleam
+++ b/src/lustre/server_component.gleam
@@ -109,7 +109,7 @@ pub fn script() -> Element(msg) {
element("script", [attribute("type", "module")], [
// <<INJECT RUNTIME>>
element.text(
- "function w(t,e,i,r=!1){let n,a=[{prev:t,next:e,parent:t.parentNode}];for(;a.length;){let{prev:s,next:o,parent:l}=a.pop();if(o.subtree!==void 0&&(o=o.subtree()),o.content!==void 0)if(s)if(s.nodeType===Node.TEXT_NODE)s.textContent!==o.content&&(s.textContent=o.content),n??=s;else{let c=document.createTextNode(o.content);l.replaceChild(c,s),n??=c}else{let c=document.createTextNode(o.content);l.appendChild(c),n??=c}else if(o.tag!==void 0){let c=D({prev:s,next:o,dispatch:i,stack:a,isComponent:r});s?s!==c&&l.replaceChild(c,s):l.appendChild(c),n??=c}else o.elements!==void 0&&x(o,c=>{a.unshift({prev:s,next:c,parent:l}),s=s?.nextSibling})}return n}function J(t,e,i){let r=t.parentNode;for(let n of e[0]){let a=n[0].split(\"-\"),s=n[1],o=k(r,a),l;if(o!==null&&o!==r)l=w(o,s,i);else{let c=k(r,a.slice(0,-1)),u=document.createTextNode(\"\");c.appendChild(u),l=w(u,s,i)}a===\"0\"&&(t=l)}for(let n of e[1]){let a=n[0].split(\"-\");k(r,a).remove()}for(let n of e[2]){let a=n[0].split(\"-\"),s=n[1],o=k(r,a),l=N.get(o);for(let c of s[0]){let u=c[0],m=c[1];if(u.startsWith(\"data-lustre-on-\")){let b=u.slice(15),h=i(M);l.has(b)||el.addEventListener(b,y),l.set(b,h),el.setAttribute(u,m)}else o.setAttribute(u,m),o[u]=m}for(let c of s[1])if(c[0].startsWith(\"data-lustre-on-\")){let u=c[0].slice(15);o.removeEventListener(u,y),l.delete(u)}else o.removeAttribute(c[0])}return t}function D({prev:t,next:e,dispatch:i,stack:r}){let n=e.namespace||\"http://www.w3.org/1999/xhtml\",a=t&&t.nodeType===Node.ELEMENT_NODE&&t.localName===e.tag&&t.namespaceURI===(e.namespace||\"http://www.w3.org/1999/xhtml\"),s=a?t:n?document.createElementNS(n,e.tag):document.createElement(e.tag),o;if(N.has(s))o=N.get(s);else{let d=new Map;N.set(s,d),o=d}let l=a?new Set(o.keys()):null,c=a?new Set(Array.from(t.attributes,d=>d.name)):null,u=null,m=null,b=null;for(let d of e.attrs){let f=d[0],p=d[1];if(d.as_property)s[f]=p;else if(f.startsWith(\"on\")){let g=f.slice(2),v=i(p);o.has(g)||s.addEventListener(g,y),o.set(g,v),a&&l.delete(g)}else if(f.startsWith(\"data-lustre-on-\")){let g=f.slice(15),v=i(M);o.has(g)||s.addEventListener(g,y),o.set(g,v),s.setAttribute(f,p)}else f===\"class\"?u=u===null?p:u+\" \"+p:f===\"style\"?m=m===null?p:m+p:f===\"dangerous-unescaped-html\"?b=p:(typeof p==\"string\"&&s.setAttribute(f,p),(f===\"value\"||f===\"selected\")&&(s[f]=p),a&&c.delete(f))}if(u!==null&&(s.setAttribute(\"class\",u),a&&c.delete(\"class\")),m!==null&&(s.setAttribute(\"style\",m),a&&c.delete(\"style\")),a){for(let d of c)s.removeAttribute(d);for(let d of l)o.delete(d),s.removeEventListener(d,y)}if(e.key!==void 0&&e.key!==\"\")s.setAttribute(\"data-lustre-key\",e.key);else if(b!==null)return s.innerHTML=b,s;let h=s.firstChild,E=null,C=null,T=null,A=e.children[Symbol.iterator]().next().value;a&&A!==void 0&&A.key!==void 0&&A.key!==\"\"&&(E=new Set,C=L(t),T=L(e));for(let d of e.children)x(d,f=>{f.key!==void 0&&E!==null?h=W(h,f,s,r,T,C,E):(r.unshift({prev:h,next:f,parent:s}),h=h?.nextSibling)});for(;h;){let d=h.nextSibling;s.removeChild(h),h=d}return s}var N=new WeakMap;function y(t){let e=t.currentTarget;if(!N.has(e)){e.removeEventListener(t.type,y);return}let i=N.get(e);if(!i.has(t.type)){e.removeEventListener(t.type,y);return}i.get(t.type)(t)}function M(t){let e=t.target,i=e.getAttribute(`data-lustre-on-${t.type}`),r=JSON.parse(e.getAttribute(\"data-lustre-data\")||\"{}\"),n=JSON.parse(e.getAttribute(\"data-lustre-include\")||\"[]\");switch(t.type){case\"input\":case\"change\":n.push(\"target.value\");break}return{tag:i,data:n.reduce((a,s)=>{let o=s.split(\".\");for(let l=0,c=a,u=t;l<o.length;l++)l===o.length-1?c[o[l]]=u[o[l]]:(c[o[l]]??={},u=u[o[l]],c=c[o[l]]);return a},{data:r})}}function L(t){let e=new Map;if(t)for(let i of t.children)x(i,r=>{let n=r?.key||r?.getAttribute?.(\"data-lustre-key\");n&&e.set(n,r)});return e}function k(t,e){let i,r,n=t;for(;[i,...r]=e,i!==void 0;)console.log({n:i,rest:r,child:n,path:e}),n=n.childNodes.item(i),e=r;return n}function W(t,e,i,r,n,a,s){for(;t&&!n.has(t.getAttribute(\"data-lustre-key\"));){let l=t.nextSibling;i.removeChild(t),t=l}if(a.size===0)return x(e,l=>{r.unshift({prev:t,next:l,parent:i}),t=t?.nextSibling}),t;if(s.has(e.key))return console.warn(`Duplicate key found in Lustre vnode: ${e.key}`),r.unshift({prev:null,next:e,parent:i}),t;s.add(e.key);let o=a.get(e.key);if(!o&&!t)return r.unshift({prev:null,next:e,parent:i}),t;if(!o&&t!==null){let l=document.createTextNode(\"\");return i.insertBefore(l,t),r.unshift({prev:l,next:e,parent:i}),t}return!o||o===t?(r.unshift({prev:t,next:e,parent:i}),t=t?.nextSibling,t):(i.insertBefore(o,t),r.unshift({prev:o,next:e,parent:i}),t)}function x(t,e){if(t.elements!==void 0)for(let i of t.elements)e(i);else e(t)}var O=class extends HTMLElement{static get observedAttributes(){return[\"route\"]}#n=null;#t=null;#e=null;constructor(){super(),this.#n=new MutationObserver(e=>{let i=[];for(let r of e)if(r.type===\"attributes\"){let{attributeName:n,oldValue:a}=r,s=this.getAttribute(n);if(a!==s)try{i.push([n,JSON.parse(s)])}catch{i.push([n,s])}}i.length&&this.#e?.send(JSON.stringify([5,i]))})}connectedCallback(){this.#t=document.createElement(\"div\"),this.appendChild(this.#t)}attributeChangedCallback(e,i,r){switch(e){case\"route\":if(!r)this.#e?.close(),this.#e=null;else if(i!==r){let n=this.getAttribute(\"id\"),a=r+(n?`?id=${n}`:\"\");this.#e?.close(),this.#e=new WebSocket(`ws://${window.location.host}${a}`),this.#e.addEventListener(\"message\",s=>this.messageReceivedCallback(s))}}}messageReceivedCallback({data:e}){let[i,...r]=JSON.parse(e);switch(i){case 0:return this.diff(r);case 1:return this.emit(r);case 2:return this.init(r)}}init([e,i]){let r=[];for(let n of e)n in this?r.push([n,this[n]]):this.hasAttribute(n)&&r.push([n,this.getAttribute(n)]),Object.defineProperty(this,n,{get(){return this[`_${n}`]??this.getAttribute(n)},set(a){let s=this[n];typeof a==\"string\"?this.setAttribute(n,a):this[`_${n}`]=a,s!==a&&this.#e?.send(JSON.stringify([5,[[n,a]]]))}});this.#n.observe(this,{attributeFilter:e,attributeOldValue:!0,attributes:!0,characterData:!1,characterDataOldValue:!1,childList:!1,subtree:!1}),this.morph(i),r.length&&this.#e?.send(JSON.stringify([5,r]))}morph(e){this.#t=w(this.#t,e,i=>r=>{let n=i(r);this.#e?.send(JSON.stringify([4,n.tag,n.data]))})}diff([e]){this.#t=J(this.#t,e,i=>r=>{let n=i(r);this.#e?.send(JSON.stringify([4,n.tag,n.data]))})}emit([e,i]){this.dispatchEvent(new CustomEvent(e,{detail:i}))}disconnectedCallback(){this.#e?.close()}};window.customElements.define(\"lustre-server-component\",O);export{O as LustreServerComponent};",
+ "function w(t,e,i,o=!1){let s,a=[{prev:t,next:e,parent:t.parentNode}];for(;a.length;){let{prev:n,next:r,parent:l}=a.pop();if(r.subtree!==void 0&&(r=r.subtree()),r.content!==void 0)if(n)if(n.nodeType===Node.TEXT_NODE)n.textContent!==r.content&&(n.textContent=r.content),s??=n;else{let c=document.createTextNode(r.content);l.replaceChild(c,n),s??=c}else{let c=document.createTextNode(r.content);l.appendChild(c),s??=c}else if(r.tag!==void 0){let c=D({prev:n,next:r,dispatch:i,stack:a,isComponent:o});n?n!==c&&l.replaceChild(c,n):l.appendChild(c),s??=c}else r.elements!==void 0?x(r,c=>{a.unshift({prev:n,next:c,parent:l}),n=n?.nextSibling}):r.subtree!==void 0&&a.push({prev:n,next:r,parent:l})}return s}function J(t,e,i){let o=t.parentNode;for(let s of e[0]){let a=s[0].split(\"-\"),n=s[1],r=k(o,a),l;if(r!==null&&r!==o)l=w(r,n,i);else{let c=k(o,a.slice(0,-1)),u=document.createTextNode(\"\");c.appendChild(u),l=w(u,n,i)}a===\"0\"&&(t=l)}for(let s of e[1]){let a=s[0].split(\"-\");k(o,a).remove()}for(let s of e[2]){let a=s[0].split(\"-\"),n=s[1],r=k(o,a),l=N.get(r);for(let c of n[0]){let u=c[0],m=c[1];if(u.startsWith(\"data-lustre-on-\")){let b=u.slice(15),h=i(M);l.has(b)||el.addEventListener(b,y),l.set(b,h),el.setAttribute(u,m)}else r.setAttribute(u,m),r[u]=m}for(let c of n[1])if(c[0].startsWith(\"data-lustre-on-\")){let u=c[0].slice(15);r.removeEventListener(u,y),l.delete(u)}else r.removeAttribute(c[0])}return t}function D({prev:t,next:e,dispatch:i,stack:o}){let s=e.namespace||\"http://www.w3.org/1999/xhtml\",a=t&&t.nodeType===Node.ELEMENT_NODE&&t.localName===e.tag&&t.namespaceURI===(e.namespace||\"http://www.w3.org/1999/xhtml\"),n=a?t:s?document.createElementNS(s,e.tag):document.createElement(e.tag),r;if(N.has(n))r=N.get(n);else{let d=new Map;N.set(n,d),r=d}let l=a?new Set(r.keys()):null,c=a?new Set(Array.from(t.attributes,d=>d.name)):null,u=null,m=null,b=null;for(let d of e.attrs){let f=d[0],p=d[1];if(d.as_property)n[f]=p,a&&c.delete(f);else if(f.startsWith(\"on\")){let g=f.slice(2),v=i(p);r.has(g)||n.addEventListener(g,y),r.set(g,v),a&&l.delete(g)}else if(f.startsWith(\"data-lustre-on-\")){let g=f.slice(15),v=i(M);r.has(g)||n.addEventListener(g,y),r.set(g,v),n.setAttribute(f,p)}else f===\"class\"?u=u===null?p:u+\" \"+p:f===\"style\"?m=m===null?p:m+p:f===\"dangerous-unescaped-html\"?b=p:(typeof p==\"string\"&&n.setAttribute(f,p),(f===\"value\"||f===\"selected\")&&(n[f]=p),a&&c.delete(f))}if(u!==null&&(n.setAttribute(\"class\",u),a&&c.delete(\"class\")),m!==null&&(n.setAttribute(\"style\",m),a&&c.delete(\"style\")),a){for(let d of c)n.removeAttribute(d);for(let d of l)r.delete(d),n.removeEventListener(d,y)}if(e.key!==void 0&&e.key!==\"\")n.setAttribute(\"data-lustre-key\",e.key);else if(b!==null)return n.innerHTML=b,n;let h=n.firstChild,E=null,C=null,T=null,A=e.children[Symbol.iterator]().next().value;a&&A!==void 0&&A.key!==void 0&&A.key!==\"\"&&(E=new Set,C=L(t),T=L(e));for(let d of e.children)x(d,f=>{f.key!==void 0&&E!==null?h=W(h,f,n,o,T,C,E):(o.unshift({prev:h,next:f,parent:n}),h=h?.nextSibling)});for(;h;){let d=h.nextSibling;n.removeChild(h),h=d}return n}var N=new WeakMap;function y(t){let e=t.currentTarget;if(!N.has(e)){e.removeEventListener(t.type,y);return}let i=N.get(e);if(!i.has(t.type)){e.removeEventListener(t.type,y);return}i.get(t.type)(t)}function M(t){let e=t.target,i=e.getAttribute(`data-lustre-on-${t.type}`),o=JSON.parse(e.getAttribute(\"data-lustre-data\")||\"{}\"),s=JSON.parse(e.getAttribute(\"data-lustre-include\")||\"[]\");switch(t.type){case\"input\":case\"change\":s.push(\"target.value\");break}return{tag:i,data:s.reduce((a,n)=>{let r=n.split(\".\");for(let l=0,c=a,u=t;l<r.length;l++)l===r.length-1?c[r[l]]=u[r[l]]:(c[r[l]]??={},u=u[r[l]],c=c[r[l]]);return a},{data:o})}}function L(t){let e=new Map;if(t)for(let i of t.children)x(i,o=>{let s=o?.key||o?.getAttribute?.(\"data-lustre-key\");s&&e.set(s,o)});return e}function k(t,e){let i,o,s=t;for(;[i,...o]=e,i!==void 0;)s=s.childNodes.item(i),e=o;return s}function W(t,e,i,o,s,a,n){for(;t&&!s.has(t.getAttribute(\"data-lustre-key\"));){let l=t.nextSibling;i.removeChild(t),t=l}if(a.size===0)return x(e,l=>{o.unshift({prev:t,next:l,parent:i}),t=t?.nextSibling}),t;if(n.has(e.key))return console.warn(`Duplicate key found in Lustre vnode: ${e.key}`),o.unshift({prev:null,next:e,parent:i}),t;n.add(e.key);let r=a.get(e.key);if(!r&&!t)return o.unshift({prev:null,next:e,parent:i}),t;if(!r&&t!==null){let l=document.createTextNode(\"\");return i.insertBefore(l,t),o.unshift({prev:l,next:e,parent:i}),t}return!r||r===t?(o.unshift({prev:t,next:e,parent:i}),t=t?.nextSibling,t):(i.insertBefore(r,t),o.unshift({prev:r,next:e,parent:i}),t)}function x(t,e){if(t.elements!==void 0)for(let i of t.elements)e(i);else e(t)}var O=class extends HTMLElement{static get observedAttributes(){return[\"route\"]}#n=null;#t=null;#e=null;constructor(){super(),this.#n=new MutationObserver(e=>{let i=[];for(let o of e)if(o.type===\"attributes\"){let{attributeName:s,oldValue:a}=o,n=this.getAttribute(s);if(a!==n)try{i.push([s,JSON.parse(n)])}catch{i.push([s,n])}}i.length&&this.#e?.send(JSON.stringify([5,i]))})}connectedCallback(){this.#t=document.createElement(\"div\"),this.appendChild(this.#t)}attributeChangedCallback(e,i,o){switch(e){case\"route\":if(!o)this.#e?.close(),this.#e=null;else if(i!==o){let s=this.getAttribute(\"id\"),a=o+(s?`?id=${s}`:\"\");this.#e?.close(),this.#e=new WebSocket(`ws://${window.location.host}${a}`),this.#e.addEventListener(\"message\",n=>this.messageReceivedCallback(n))}}}messageReceivedCallback({data:e}){let[i,...o]=JSON.parse(e);switch(i){case 0:return this.diff(o);case 1:return this.emit(o);case 2:return this.init(o)}}init([e,i]){let o=[];for(let s of e)s in this?o.push([s,this[s]]):this.hasAttribute(s)&&o.push([s,this.getAttribute(s)]),Object.defineProperty(this,s,{get(){return this[`_${s}`]??this.getAttribute(s)},set(a){let n=this[s];typeof a==\"string\"?this.setAttribute(s,a):this[`_${s}`]=a,n!==a&&this.#e?.send(JSON.stringify([5,[[s,a]]]))}});this.#n.observe(this,{attributeFilter:e,attributeOldValue:!0,attributes:!0,characterData:!1,characterDataOldValue:!1,childList:!1,subtree:!1}),this.morph(i),o.length&&this.#e?.send(JSON.stringify([5,o]))}morph(e){this.#t=w(this.#t,e,i=>o=>{let s=i(o);this.#e?.send(JSON.stringify([4,s.tag,s.data]))})}diff([e]){this.#t=J(this.#t,e,i=>o=>{let s=i(o);this.#e?.send(JSON.stringify([4,s.tag,s.data]))})}emit([e,i]){this.dispatchEvent(new CustomEvent(e,{detail:i}))}disconnectedCallback(){this.#e?.close()}};window.customElements.define(\"lustre-server-component\",O);export{O as LustreServerComponent};",
),
])
}