diff --git a/.changeset/fair-jobs-punch.md b/.changeset/fair-jobs-punch.md new file mode 100644 index 0000000000..78ed1d4bd7 --- /dev/null +++ b/.changeset/fair-jobs-punch.md @@ -0,0 +1,5 @@ +--- +"@marko/runtime-tags": patch +--- + +Fix async runtime placeholder duplication and race condition. diff --git a/packages/runtime-tags/src/__tests__/fixtures/catch-single-reject-async/__snapshots__/ssr-sanitized.expected.md b/packages/runtime-tags/src/__tests__/fixtures/catch-single-reject-async/__snapshots__/ssr-sanitized.expected.md index d608f511de..26c7d8fab4 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/catch-single-reject-async/__snapshots__/ssr-sanitized.expected.md +++ b/packages/runtime-tags/src/__tests__/fixtures/catch-single-reject-async/__snapshots__/ssr-sanitized.expected.md @@ -1,4 +1,4 @@ # Render "End" ```html -aERROR!efg +aERROR!def ``` \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/catch-single-reject-async/__snapshots__/ssr.expected.md b/packages/runtime-tags/src/__tests__/fixtures/catch-single-reject-async/__snapshots__/ssr.expected.md index f620a1c409..ff12ca75bc 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/catch-single-reject-async/__snapshots__/ssr.expected.md +++ b/packages/runtime-tags/src/__tests__/fixtures/catch-single-reject-async/__snapshots__/ssr.expected.md @@ -3,7 +3,7 @@ # Write - efgERROR! + defERROR! # Render "End" @@ -17,7 +17,7 @@ - aERROR!efg + aERROR!def diff --git a/packages/runtime-tags/src/__tests__/fixtures/catch-single-reject-async/server.ts b/packages/runtime-tags/src/__tests__/fixtures/catch-single-reject-async/server.ts index a0355bdad1..a3560ad050 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/catch-single-reject-async/server.ts +++ b/packages/runtime-tags/src/__tests__/fixtures/catch-single-reject-async/server.ts @@ -13,15 +13,15 @@ const renderer = () => { () => { write("b"); fork(rejectAfter(new Error("ERROR!"), 2), write); - write("d"); + write("c"); }, (err) => { write((err as Error).message); }, ); - write("e"); - fork(resolveAfter("f", 1), write); - write("g"); + write("d"); + fork(resolveAfter("e", 1), write); + write("f"); }; export default createTemplate("", renderer); diff --git a/packages/runtime-tags/src/__tests__/fixtures/component-markers-placeholder.skip/server.ts b/packages/runtime-tags/src/__tests__/fixtures/component-markers-placeholder.skip/server.ts index 4e46a19a1e..6dbc2f9fa3 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/component-markers-placeholder.skip/server.ts +++ b/packages/runtime-tags/src/__tests__/fixtures/component-markers-placeholder.skip/server.ts @@ -21,7 +21,7 @@ const firstComponent = () => { write("d"); }, () => { - write("e..."); + write("_A_"); }, ); write("e"); @@ -30,18 +30,18 @@ const firstComponent = () => { }; const secondComponent = () => { - write("v"); + write("1"); tryPlaceholder( () => { - write("w"); - fork(resolveAfter("x", 2), write); - write("y"); + write("2"); + fork(resolveAfter("3", 2), write); + write("4"); }, () => { - write("z..."); + write("_B_"); }, ); - write("z"); + write("5"); }; export default createTemplate("", renderer); diff --git a/packages/runtime-tags/src/__tests__/fixtures/placeholder-single/__snapshots__/ssr-sanitized.expected.md b/packages/runtime-tags/src/__tests__/fixtures/placeholder-single/__snapshots__/ssr-sanitized.expected.md index ed9f2dca39..6af7eb33af 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/placeholder-single/__snapshots__/ssr-sanitized.expected.md +++ b/packages/runtime-tags/src/__tests__/fixtures/placeholder-single/__snapshots__/ssr-sanitized.expected.md @@ -1,10 +1,4 @@ # Render "End" ```html -abdfgh - - cd - +abcdefg ``` \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/placeholder-single/__snapshots__/ssr.expected.md b/packages/runtime-tags/src/__tests__/fixtures/placeholder-single/__snapshots__/ssr.expected.md index aa07adb0ca..ffe40ddbf6 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/placeholder-single/__snapshots__/ssr.expected.md +++ b/packages/runtime-tags/src/__tests__/fixtures/placeholder-single/__snapshots__/ssr.expected.md @@ -1,13 +1,13 @@ # Write - ae...fbd + a_A_ebd # Write - gh + fg # Write - cd + c # Render "End" @@ -21,19 +21,11 @@ - ab - - df + abcde - gh - - cd - + fg @@ -55,23 +47,27 @@ inserted #document/html0/head0/style0 inserted #document/html0/head0/style0/#text0 inserted t inserted #document/html0/body1/#text1 -inserted #document/html0/body1/#comment2 +inserted #comment inserted #document/html0/body1/#text3 inserted #document/html0/body1/script5 inserted #document/html0/body1/script5/#text0 removed #document/html0/head0/style0 after #document/html0/body1/#text4 inserted #document/html0/head0/style0 inserted #document/html0/body1/#text6 -inserted #document/html0/body1/t7 -inserted #document/html0/body1/t7/#text0 -inserted #document/html0/body1/script8 -inserted #document/html0/body1/script8/#text0 +inserted t +inserted #document/html0/body1/#text2 +inserted #document/html0/body1/script7 +inserted #document/html0/body1/script7/#text0 +removed #document/html0/body1/#text2 in t +removed #comment after #document/html0/body1/#text1 +inserted #document/html0/body1/#text2 +removed t after #document/html0/body1/#text6 removed #text after #comment removed #comment after #comment -removed #document/html0/body1/#text1 before #document/html0/body1/#comment2 -removed #document/html0/body1/#comment2 before #document/html0/body1/#text3 +removed #document/html0/body1/#text1 before #document/html0/body1/#text2 +removed #document/html0/body1/#text2 before #document/html0/body1/#text3 removed #document/html0/body1/#text3 in t removed #comment after #document/html0/body1/#text0 -inserted #document/html0/body1/#text1, #document/html0/body1/#comment2, #document/html0/body1/#text3 +inserted #document/html0/body1/#text1, #document/html0/body1/#text2, #document/html0/body1/#text3 removed t after #document/html0/body1/#text4 ``` \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/placeholder-single/server.ts b/packages/runtime-tags/src/__tests__/fixtures/placeholder-single/server.ts index a0b47fe338..dfa1b2a631 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/placeholder-single/server.ts +++ b/packages/runtime-tags/src/__tests__/fixtures/placeholder-single/server.ts @@ -16,12 +16,12 @@ const renderer = () => { write("d"); }, () => { - write("e..."); + write("_A_"); }, ); - write("f"); - fork(resolveAfter("g", 1), write); - write("h"); + write("e"); + fork(resolveAfter("f", 1), write); + write("g"); }; export default createTemplate("", renderer); diff --git a/packages/runtime-tags/src/__tests__/fixtures/placeholder-skipped/__snapshots__/ssr-sanitized.expected.md b/packages/runtime-tags/src/__tests__/fixtures/placeholder-skipped/__snapshots__/ssr-sanitized.expected.md index d5df79d24e..ca7712944b 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/placeholder-skipped/__snapshots__/ssr-sanitized.expected.md +++ b/packages/runtime-tags/src/__tests__/fixtures/placeholder-skipped/__snapshots__/ssr-sanitized.expected.md @@ -1,4 +1,4 @@ # Render "End" ```html -abdef +abcde ``` \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/placeholder-skipped/__snapshots__/ssr.expected.md b/packages/runtime-tags/src/__tests__/fixtures/placeholder-skipped/__snapshots__/ssr.expected.md index e5af9491f6..75637958ea 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/placeholder-skipped/__snapshots__/ssr.expected.md +++ b/packages/runtime-tags/src/__tests__/fixtures/placeholder-skipped/__snapshots__/ssr.expected.md @@ -1,9 +1,9 @@ # Write - abd + abc # Write - ef + de # Render "End" @@ -11,7 +11,7 @@ - abdef + abcde ``` diff --git a/packages/runtime-tags/src/__tests__/fixtures/placeholder-skipped/server.ts b/packages/runtime-tags/src/__tests__/fixtures/placeholder-skipped/server.ts index f1d12fda86..9cd2f42e89 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/placeholder-skipped/server.ts +++ b/packages/runtime-tags/src/__tests__/fixtures/placeholder-skipped/server.ts @@ -14,12 +14,12 @@ const renderer = () => { write("b"); }, () => { - write("c..."); + write("_A_"); }, ); - write("d"); - fork(resolveAfter("e", 1), write); - write("f"); + write("c"); + fork(resolveAfter("d", 1), write); + write("e"); }; export default createTemplate("", renderer); diff --git a/packages/runtime-tags/src/__tests__/fixtures/placeholders-nested/__snapshots__/ssr-sanitized.expected.md b/packages/runtime-tags/src/__tests__/fixtures/placeholders-nested/__snapshots__/ssr-sanitized.expected.md index 12f34ee5b6..0ab84c1a02 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/placeholders-nested/__snapshots__/ssr-sanitized.expected.md +++ b/packages/runtime-tags/src/__tests__/fixtures/placeholders-nested/__snapshots__/ssr-sanitized.expected.md @@ -1,10 +1,4 @@ # Render "End" ```html -abdeg - - fg - +abcdefghij ``` \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/placeholders-nested/__snapshots__/ssr.expected.md b/packages/runtime-tags/src/__tests__/fixtures/placeholders-nested/__snapshots__/ssr.expected.md index 068afdc9b9..3ff0b9658b 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/placeholders-nested/__snapshots__/ssr.expected.md +++ b/packages/runtime-tags/src/__tests__/fixtures/placeholders-nested/__snapshots__/ssr.expected.md @@ -1,17 +1,17 @@ # Write - ai...jbdh...eg + a_B_hbd_A_eg # Write - kl + ij # Write - cdh... + c # Write - fg + f # Render "End" @@ -25,20 +25,14 @@ - ab - - de - - g + abcdefgh + + ij - - fg - @@ -55,54 +49,58 @@ inserted #document/html0/body1/#text0 inserted #comment inserted #text inserted #comment -inserted #text +inserted #document/html0/body1/#text7 inserted #document/html0/head0/style0 inserted #document/html0/head0/style0/#text0 inserted t inserted #document/html0/body1/#text1 -inserted #document/html0/body1/#comment2 +inserted #comment inserted #document/html0/body1/#text3 inserted #comment inserted #text inserted #comment inserted t inserted #document/html0/body1/#text4 -inserted #document/html0/body1/#comment5 +inserted #comment inserted #document/html0/body1/#text6 -inserted script -inserted script/#text0 -removed #document/html0/head0/style0 after #text +inserted #document/html0/body1/script8 +inserted #document/html0/body1/script8/#text0 +removed #document/html0/head0/style0 after #document/html0/body1/#text7 inserted #document/html0/head0/style0 -inserted #text +inserted #document/html0/body1/#text9 inserted t -inserted t/#text0 -inserted t/#comment1 -inserted t/#text2 -inserted t/#comment3 -inserted #document/html0/body1/script7 -inserted #document/html0/body1/script7/#text0 +inserted #document/html0/body1/#text2 +inserted #document/html0/body1/script10 +inserted #document/html0/body1/script10/#text0 +removed #document/html0/body1/#text2 in t +removed #comment after #document/html0/body1/#text1 +inserted #document/html0/body1/#text2 +removed t after #document/html0/body1/#text9 removed #text after #comment removed #comment after #comment -removed #document/html0/body1/#text1 before #document/html0/body1/#comment2 -removed #document/html0/body1/#comment2 before #document/html0/body1/#text3 +removed #document/html0/body1/#text1 before #document/html0/body1/#text2 +removed #document/html0/body1/#text2 before #document/html0/body1/#text3 removed #document/html0/body1/#text3 before #comment removed #comment before #text removed #text before #comment removed #comment in t removed #comment after #document/html0/body1/#text0 -inserted #document/html0/body1/#text1, #document/html0/body1/#comment2, #document/html0/body1/#text3, #comment, #text, #comment -removed t after #text -inserted #document/html0/body1/t8 -inserted #document/html0/body1/t8/#text0 -inserted #document/html0/body1/script9 -inserted #document/html0/body1/script9/#text0 +inserted #document/html0/body1/#text1, #document/html0/body1/#text2, #document/html0/body1/#text3, #comment, #text, #comment +removed t after #document/html0/body1/#text7 +inserted t +inserted #document/html0/body1/#text5 +inserted #document/html0/body1/script11 +inserted #document/html0/body1/script11/#text0 +removed #document/html0/body1/#text5 in t +removed #comment after #document/html0/body1/#text4 +inserted #document/html0/body1/#text5 +removed t after #document/html0/body1/script10 removed #text after #comment removed #comment after #comment -removed #text after #comment -removed t after #comment -removed script after #comment -removed #text after #comment -removed t after #comment +removed #document/html0/body1/#text4 before #document/html0/body1/#text5 +removed #document/html0/body1/#text5 before #document/html0/body1/#text6 +removed #document/html0/body1/#text6 in t removed #comment after #document/html0/body1/#text3 -inserted #document/html0/body1/#text4, #document/html0/body1/#comment5, #document/html0/body1/#text6 +inserted #document/html0/body1/#text4, #document/html0/body1/#text5, #document/html0/body1/#text6 +removed t after #document/html0/body1/#text7 ``` \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/placeholders-nested/server.ts b/packages/runtime-tags/src/__tests__/fixtures/placeholders-nested/server.ts index 652081b3b2..7020798b1d 100644 --- a/packages/runtime-tags/src/__tests__/fixtures/placeholders-nested/server.ts +++ b/packages/runtime-tags/src/__tests__/fixtures/placeholders-nested/server.ts @@ -21,17 +21,17 @@ const renderer = () => { write("g"); }, () => { - write("h..."); + write("_A_"); }, ); }, () => { - write("i..."); + write("_B_"); }, ); + write("h"); + fork(resolveAfter("i", 1), write); write("j"); - fork(resolveAfter("k", 1), write); - write("l"); }; export default createTemplate("", renderer); diff --git a/packages/runtime-tags/src/html/inlined-runtimes.ts b/packages/runtime-tags/src/html/inlined-runtimes.ts index bd4d3965c4..f02b3019de 100644 --- a/packages/runtime-tags/src/html/inlined-runtimes.ts +++ b/packages/runtime-tags/src/html/inlined-runtimes.ts @@ -1,5 +1,5 @@ export const WALKER_RUNTIME_CODE = MARKO_DEBUG - ? `((runtimeId) => + ? /* js */ `((runtimeId) => (self[runtimeId] = self[runtimeId] || ((renderId) => { @@ -36,13 +36,12 @@ export const WALKER_RUNTIME_CODE = MARKO_DEBUG }), prefix = renderId.length; })))` - : `(e=>self[e]=self[e]||(l=>{let t,d={},f=[],s=document,a=s.createTreeWalker(s,129),r=self[e][l]={i:l=e+l,d:s,l:d,v:f,x(){},w(e){for(;e=a.nextNode();)this.x(r=(r=e.data)&&!r.indexOf(l)&&(d[t=r.slice(x+1)]=e,r[x]),t,e),r>"#"&&f.push(e)}},x=l.length}))`; + : `(e=>self[e]=self[e]||(l=>{let t,d={},s=[],f=document,i=f.createTreeWalker(f,129),n=self[e][l]={i:l=e+l,d:f,l:d,v:s,x(){},w(e){for(;e=i.nextNode();)this.x(n=(n=e.data)&&!n.indexOf(l)&&(d[t=n.slice(x+1)]=e,n[x]),t,e),n>"#"&&s.push(e)}},x=l.length}))`; export const REORDER_RUNTIME_CODE = MARKO_DEBUG - ? `((runtime) => { -let insertOne, + ? /* js */ `((runtime) => { +let onNextSibling, placeholder, nextSibling, - previousSibling, placeholders = {}, replace = (marker, container) => { marker.replaceWith(...container.childNodes); @@ -53,21 +52,19 @@ runtime.d.head.append( ); runtime.j = {}; runtime.x = (op, id, node, start, placeholderCallback) => { - // "node" and "end" are all closed over and can't be repurposed. "start" is too but only in the new placeholder case - if (op == "#") { (placeholders[id] = placeholder).i++; } else if (node == nextSibling) { - insertOne(); + onNextSibling(); } if (node.tagName == "T" && (id = node.getAttribute(runtime.i))) { start = runtime.l["^" + id]; if (start) { - placeholder = placeholders[id] = { - i: 0, - c(end = runtime.l[id] || previousSibling || node) { + placeholders[id] = { + i: 1, + c(end = runtime.l[id] || node) { while (end.parentNode !== start.parentNode) { end = end.parentNode; } @@ -80,27 +77,23 @@ runtime.x = (op, id, node, start, placeholderCallback) => { replace(start, node); }, }; - } else { - insertOne = () => { - previousSibling = node.previousSibling; - replace(runtime.l[id], node); - if (!--start.i) { - start.c(); - } - }; - - // repurpose "start" to hold this placeholder - start = placeholder = placeholders[id]; - nextSibling = node.nextElementSibling || insertOne(); } - // repurpose "op" for callbacks ...carefully - placeholderCallback = placeholder.c; - (op = runtime.j[id]) && - (placeholder.c = () => placeholderCallback() + op(runtime)); + nextSibling = node.nextSibling; + placeholder = placeholders[id]; + onNextSibling = () => { + start || replace(runtime.l[id], node); + if (!--placeholder.i) { + placeholder.c(); + } + }; - if (node.attributes.c) placeholder.c(); + // repurpose "op" for callbacks ...carefully + if (op = runtime.j[id]) { + placeholderCallback = placeholder.c; + placeholder.c = () => placeholderCallback() + op(runtime); + } } }; })` - : `(e=>{let i,t,r,l,d={},n=(e,i)=>{e.replaceWith(...i.childNodes),i.remove()};e.d.head.append(e.d.querySelector("style["+e.i+"]")||""),e.j={},e.x=(o,a,c,p,b)=>{"#"==o?(d[a]=t).i++:c==r&&i(),"T"==c.tagName&&(a=c.getAttribute(e.i))&&((p=e.l["^"+a])?t=d[a]={i:0,c(i=e.l[a]||l||c){for(;i.parentNode!==p.parentNode;)i=i.parentNode;for(;i!=r;(r=p.nextSibling).remove());n(p,c)}}:(i=()=>{l=c.previousSibling,n(e.l[a],c),--p.i||p.c()},p=t=d[a],r=c.nextElementSibling||i()),b=t.c,(o=e.j[a])&&(t.c=()=>b()+o(e)),c.attributes.c&&t.c())}})`; + : `(e=>{let i,t,r,l={},o=(e,i)=>{e.replaceWith(...i.childNodes),i.remove()};e.d.head.append(e.d.querySelector("style["+e.i+"]")||""),e.j={},e.x=(d,n,a,c,p)=>{"#"==d?(l[n]=t).i++:a==r&&i(),"T"==a.tagName&&(n=a.getAttribute(e.i))&&((c=e.l["^"+n])&&(l[n]={i:1,c(i=e.l[n]||a){for(;i.parentNode!==c.parentNode;)i=i.parentNode;for(;i!=r;(r=c.nextSibling).remove());o(c,a)}}),r=a.nextSibling,t=l[n],i=()=>{c||o(e.l[n],a),--t.i||t.c()},(d=e.j[n])&&(p=t.c,t.c=()=>p()+d(e)))}})`; diff --git a/packages/runtime-tags/src/html/writer.ts b/packages/runtime-tags/src/html/writer.ts index 2cc30fac0e..4abd31d6a9 100644 --- a/packages/runtime-tags/src/html/writer.ts +++ b/packages/runtime-tags/src/html/writer.ts @@ -610,7 +610,6 @@ export function prepareChunk(chunk: Chunk) { for (const reorderedChunk of state.writeReorders) { const { reorderId } = reorderedChunk; - let isSync = true; let reorderHTML = ""; let reorderEffects = ""; let reorderScripts = ""; @@ -618,7 +617,9 @@ export function prepareChunk(chunk: Chunk) { reorderedChunk.reorderId = null; for (;;) { + const { next } = cur; cur.flushPlaceholder(); + cur.consumed = true; reorderHTML += cur.html; reorderEffects = concatEffects(reorderEffects, cur.effects); reorderScripts = concatScripts(reorderScripts, cur.scripts); @@ -629,11 +630,11 @@ export function prepareChunk(chunk: Chunk) { (cur.reorderId = state.nextReorderId()), ); cur.html = cur.effects = cur.scripts = ""; - isSync = false; + cur.next = null; } - if (cur.next) { - cur = cur.next; + if (next) { + cur = next; } else { break; } @@ -668,7 +669,6 @@ export function prepareChunk(chunk: Chunk) { html += "