Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/legal-trains-show.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@qwik.dev/core': patch
---

fix: correctly handle interactivty during html streaming
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So before this change it only captured user events after the end of the stream?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, but it could resume qwik before the state streamed

40 changes: 40 additions & 0 deletions packages/qwik/src/qwikloader.behavior.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ function createEventTarget() {
listeners.set(eventName, registrations);
}
),
removeEventListener: vi.fn((eventName: string, handler: (ev: any) => unknown) => {
const registrations = listeners.get(eventName) ?? [];
listeners.set(
eventName,
registrations.filter((registration) => registration.handler !== handler)
);
}),
dispatchEvent: vi.fn(),
querySelectorAll: vi.fn(() => [] as any[]),
};
Expand Down Expand Up @@ -343,4 +350,37 @@ describe('qwikloader behavior', () => {
resolveChild();
await result;
});

test('waits for streamed container data before running qrl attributes', async () => {
const { doc } = createLoaderEnvironment(['e:click']);
const logs: string[] = [];
const container = createMockElement(null, {
'q:container': 'paused',
'q:base': './',
'q:instance': 'sync',
});
const button = createMockElement(container, {
'q-e:click': '#0#',
});

getSingleListener(doc, 'click').handler(createMockEvent(button));

expect(logs).toEqual([]);
expect(doc.dispatchEvent).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'qerror' }));

doc.qFuncs_sync = [
() => {
logs.push('clicked');
},
];
doc.readyState = 'interactive';
const listeners = getListeners(doc, 'readystatechange');
for (let i = 0; i < listeners.length; i++) {
listeners[i]!.handler(createMockEvent(doc, 'readystatechange'));
}
await Promise.resolve();
await Promise.resolve();

expect(logs).toEqual(['clicked']);
});
});
34 changes: 30 additions & 4 deletions packages/qwik/src/qwikloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ const elementPrefix = 'e';
const passiveElementPrefix = 'ep';
const capturePrefix = 'capture:';

const readyStateChange = 'readystatechange';

const events = new Set<string>();
const roots = new Set<EventTarget & ParentNode>([doc]);
const symbols = new Map<string, Handler>();
Expand Down Expand Up @@ -108,6 +110,17 @@ const resolveContainer = (containerEl: QContainerElement) => {
}
};

const waitForContainerReady = (container: QContainerElement) =>
container.getAttribute('q:container') === 'paused' && doc.readyState === 'loading'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to add code that calls container.ready() after the state. That way, a container can resume while the HTML is still streaming, like in multi containers

? new Promise<void>((resolve) => {
const done = () => {
doc.removeEventListener(readyStateChange, done);
resolve();
};
addEventListener(doc, readyStateChange, done);
})
: undefined;

const createEvent = <T extends CustomEvent = any>(eventName: string, detail?: T['detail']) =>
new CustomEvent(eventName, { detail }) as T;

Expand Down Expand Up @@ -289,6 +302,7 @@ const dispatch = (
const qBase = container.getAttribute('q:base')!;
const base = new URL(qBase, doc.baseURI);
const qrls = attrValue.split('|');
const waitForReady = waitForContainerReady(container);
for (let i = 0; i < qrls.length; i++) {
const qrl = qrls[i];
const reqTime = performance.now();
Expand Down Expand Up @@ -319,11 +333,23 @@ const dispatch = (
}
}
};
const handler = resolveHandler(container, element, qBase, base, chunk, symbol, reqTime);
if (defer || isPromise(handler)) {
const resolve = () => resolveHandler(container, element, qBase, base, chunk, symbol, reqTime);
const handler = waitForReady && chunk === '' ? undefined : resolve();
if (isPromise(handler)) {
defer = true;
tasks.push(async () => {
await run(isPromise(handler) ? await handler : handler);
if (waitForReady) {
await waitForReady;
}
await run(await handler);
});
} else if (defer || waitForReady) {
defer = true;
tasks.push(async () => {
if (waitForReady) {
await waitForReady;
}
await run(handler || (await resolve()));
});
} else {
const result = run(handler);
Expand Down Expand Up @@ -593,6 +619,6 @@ if (!_qwikEv?.roots) {
roots,
push: addEventOrRoot,
};
addEventListener(doc, 'readystatechange', processReadyStateChange);
addEventListener(doc, readyStateChange, processReadyStateChange);
processReadyStateChange();
}
6 changes: 3 additions & 3 deletions packages/qwik/src/qwikloader.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ test('qwikloader script', () => {
const compressed = compress(Buffer.from(qwikLoader), { mode: 1, quality: 11 });
expect([compressed.length, qwikLoader.length]).toMatchInlineSnapshot(`
[
1943,
4913,
2028,
5173,
]
`);

expect(qwikLoader).toMatchInlineSnapshot(
`"const e=document,t=window,r="w",n="wp",o="d",s="dp",i="e",c="ep",l="capture:",a=new Set,p=new Set([e]),q=new Map;let u,f,h;const d=(e,t)=>Array.from(e.querySelectorAll(t)),b=e=>{const t=[];return p.forEach(r=>t.push(...d(r,e))),t},m=(e,t,r,n=!1,o=!1)=>e.addEventListener(t,r,{capture:n,passive:o}),g=e=>{J(e);const t=d(e,"[q\\\\:shadowroot]");for(let e=0;e<t.length;e++){const r=t[e].shadowRoot;r&&g(r)}},v=e=>e&&"function"==typeof e.then,y=async e=>{for(let t=0;t<e.length;t++)await e[t]()},w=e=>{if(e.length){const t=()=>y(e);h=h?h.then(t,t):t()}},E=t=>{if(void 0===t._qwikjson_){let r=(t===e.documentElement?e.body:t).lastElementChild;for(;r;){if("SCRIPT"===r.tagName&&"qwik/json"===r.getAttribute("type")){t._qwikjson_=JSON.parse(r.textContent.replace(/\\\\x3C(\\/?script)/gi,"<$1"));break}r=r.previousElementSibling}}},A=(e,t)=>new CustomEvent(e,{detail:t}),C=(t,r)=>{e.dispatchEvent(A(t,r))},_=e=>e.replace(/([A-Z-])/g,e=>"-"+e.toLowerCase()),k=e=>e.replace(/-./g,e=>e[1].toUpperCase()),B=e=>{const t=e.indexOf(":");return{scope:e.slice(0,t),eventName:k(e.slice(t+1))}},S=e=>2===e.length,I=e=>e.charAt(0),N=e=>!!e&&1===e.nodeType,T=(e,t,r)=>e.hasAttribute(r)&&(!!e._qDispatch?.[t]||e.hasAttribute("q-"+t)),$=(t,r,n,o,s,i,c)=>{const l={qBase:n,symbol:i,element:r,reqTime:c};if(""===s){const r=t.getAttribute("q:instance"),n=(e["qFuncs_"+r]||[])[Number.parseInt(i)];if(!n){const e=Error("sym:"+i);C("qerror",{importError:"sync",error:e,...l}),console.error(e)}return n}const a=\`\${i}|\${n}|\${s}\`,p=q.get(a);if(p)return p;const u=new URL(s,o).href,f=import(u);return E(t),f.then(e=>{const t=e[i];if(t)q.set(a,t),C("qsymbol",l);else{const e=Error(\`\${i} not in \${u}\`);C("qerror",{importError:"no-symbol",error:e,...l}),console.error(e)}return t},e=>{C("qerror",{importError:"async",error:e,...l}),console.error(e)})},R=(t,r,n,o,s,i=!0)=>{let c=!1;s&&(i&&t.hasAttribute("preventdefault:"+s)&&r.preventDefault(),t.hasAttribute("stoppropagation:"+s)&&r.stopPropagation());const l=t._qDispatch?.[n];if(l){if("function"==typeof l){const e=()=>l(r,t);if(c)o.push(async()=>{const t=e();v(t)&&await t});else{const t=e();v(t)&&(c=!0,o.push(()=>t))}}else if(l.length)for(let e=0;e<l.length;e++){const n=l[e];if(n){const e=()=>n(r,t);if(c)o.push(async()=>{const t=e();v(t)&&await t});else{const t=e();v(t)&&(c=!0,o.push(()=>t))}}}return}const a=t.getAttribute("q-"+n);if(a){const n=t.closest("[q\\\\:container]:not([q\\\\:container=html]):not([q\\\\:container=text])"),s=n.getAttribute("q:base"),i=new URL(s,e.baseURI),l=a.split("|");for(let e=0;e<l.length;e++){const a=l[e],p=performance.now(),[q,u,f]=a.split("#"),h=e=>{if(e&&t.isConnected)try{const n=e.call(f,r,t);if(v(n))return n.catch(e=>{C("qerror",{error:e,qBase:s,symbol:u,element:t,reqTime:p})})}catch(e){C("qerror",{error:e,qBase:s,symbol:u,element:t,reqTime:p})}},d=$(n,t,s,i,q,u,p);if(c||v(d))c=!0,o.push(async()=>{await h(v(d)?await d:d)});else{const e=h(d);v(e)&&(c=!0,o.push(()=>e))}}}},x=(e,t=i,r=!0)=>{const n=_(e.type),o=t+":"+n,s=l+n,c=[],a=[],p=[];let q=e.target;for(;q;)N(q)?(c.push(q),a.push(T(q,o,s)),q=q.parentElement):q=q.parentElement;for(let t=c.length-1;t>=0;t--)if(a[t]&&(R(c[t],e,o,p,n,r),e.cancelBubble||e.cancelBubble))return void w(p);for(let t=0;t<c.length;t++)if(!a[t]&&(R(c[t],e,o,p,n,r),!e.bubbles||e.cancelBubble||e.cancelBubble))return void w(p);w(p)},L=e=>x(e,c,!1),U=(e,t,r=!0)=>{const n=_(t.type),o=e+":"+n,s=b("[q-"+e+"\\\\:"+n+"]"),i=[];for(let e=0;e<s.length;e++){const c=s[e];R(c,t,o,i,n,r)}w(i)},j=e=>{U(o,e)},D=e=>{U(s,e,!1)},O=e=>{U(r,e)},P=e=>{U(n,e,!1)},F=()=>{const r=e.readyState;if("interactive"==r||"complete"==r){if(f=1,p.forEach(g),a.has("d:qinit")){a.delete("d:qinit");const e=A("qinit"),t=b("[q-d\\\\:qinit]"),r=[];for(let n=0;n<t.length;n++){const o=t[n];R(o,e,"d:qinit",r),o.removeAttribute("q-d:qinit")}w(r)}if(a.has("d:qidle")&&(a.delete("d:qidle"),(t.requestIdleCallback??t.setTimeout).bind(t)(()=>{const e=A("qidle"),t=b("[q-d\\\\:qidle]"),r=[];for(let n=0;n<t.length;n++){const o=t[n];R(o,e,"d:qidle",r),o.removeAttribute("q-d:qidle")}w(r)})),a.has("e:qvisible")){u||(u=new IntersectionObserver(e=>{const t=[];for(let r=0;r<e.length;r++){const n=e[r];n.isIntersecting&&(u.unobserve(n.target),R(n.target,A("qvisible",n),"e:qvisible",t))}w(t)}));const e=b("[q-e\\\\:qvisible]:not([q\\\\:observed])");for(let t=0;t<e.length;t++){const r=e[t];u.observe(r),r.setAttribute("q:observed","true")}}}},J=(...e)=>{for(let n=0;n<e.length;n++){const s=e[n];if("string"==typeof s){if(!a.has(s)){a.add(s);const{scope:e,eventName:n}=B(s),i=S(e),c=I(e);c===r?m(t,n,i?P:O,!0,i):p.forEach(e=>m(e,n,c===o?i?D:j:i?L:x,!0,i)),1!==f||"e:qvisible"!==s&&"d:qinit"!==s&&"d:qidle"!==s||F()}}else p.has(s)||(a.forEach(e=>{const{scope:t,eventName:n}=B(e),i=S(t),c=I(t);c!==r&&m(s,n,c===o?i?D:j:i?L:x,!0,i)}),p.add(s))}},M=t._qwikEv;M?.roots||(Array.isArray(M)?J(...M):J("e:click","e:input"),t._qwikEv={events:a,roots:p,push:J},m(e,"readystatechange",F),F());"`
`"const e=document,t=window,r="w",n="wp",o="d",s="dp",i="e",a="ep",c="capture:",l="readystatechange",p=new Set,q=new Set([e]),u=new Map;let d,f,h;const b=(e,t)=>Array.from(e.querySelectorAll(t)),m=e=>{const t=[];return q.forEach(r=>t.push(...b(r,e))),t},g=(e,t,r,n=!1,o=!1)=>e.addEventListener(t,r,{capture:n,passive:o}),v=e=>{Z(e);const t=b(e,"[q\\\\:shadowroot]");for(let e=0;e<t.length;e++){const r=t[e].shadowRoot;r&&v(r)}},w=e=>e&&"function"==typeof e.then,y=async e=>{for(let t=0;t<e.length;t++)await e[t]()},E=e=>{if(e.length){const t=()=>y(e);h=h?h.then(t,t):t()}},A=t=>{if(void 0===t._qwikjson_){let r=(t===e.documentElement?e.body:t).lastElementChild;for(;r;){if("SCRIPT"===r.tagName&&"qwik/json"===r.getAttribute("type")){t._qwikjson_=JSON.parse(r.textContent.replace(/\\\\x3C(\\/?script)/gi,"<$1"));break}r=r.previousElementSibling}}},C=t=>"paused"===t.getAttribute("q:container")&&"loading"===e.readyState?new Promise(t=>{const r=()=>{e.removeEventListener(l,r),t()};g(e,l,r)}):void 0,_=(e,t)=>new CustomEvent(e,{detail:t}),k=(t,r)=>{e.dispatchEvent(_(t,r))},S=e=>e.replace(/([A-Z-])/g,e=>"-"+e.toLowerCase()),B=e=>e.replace(/-./g,e=>e[1].toUpperCase()),I=e=>{const t=e.indexOf(":");return{scope:e.slice(0,t),eventName:B(e.slice(t+1))}},N=e=>2===e.length,T=e=>e.charAt(0),$=e=>!!e&&1===e.nodeType,L=(e,t,r)=>e.hasAttribute(r)&&(!!e._qDispatch?.[t]||e.hasAttribute("q-"+t)),R=(t,r,n,o,s,i,a)=>{const c={qBase:n,symbol:i,element:r,reqTime:a};if(""===s){const r=t.getAttribute("q:instance"),n=(e["qFuncs_"+r]||[])[Number.parseInt(i)];if(!n){const e=Error("sym:"+i);k("qerror",{importError:"sync",error:e,...c}),console.error(e)}return n}const l=\`\${i}|\${n}|\${s}\`,p=u.get(l);if(p)return p;const q=new URL(s,o).href,d=import(q);return A(t),d.then(e=>{const t=e[i];if(t)u.set(l,t),k("qsymbol",c);else{const e=Error(\`\${i} not in \${q}\`);k("qerror",{importError:"no-symbol",error:e,...c}),console.error(e)}return t},e=>{k("qerror",{importError:"async",error:e,...c}),console.error(e)})},x=(t,r,n,o,s,i=!0)=>{let a=!1;s&&(i&&t.hasAttribute("preventdefault:"+s)&&r.preventDefault(),t.hasAttribute("stoppropagation:"+s)&&r.stopPropagation());const c=t._qDispatch?.[n];if(c){if("function"==typeof c){const e=()=>c(r,t);if(a)o.push(async()=>{const t=e();w(t)&&await t});else{const t=e();w(t)&&(a=!0,o.push(()=>t))}}else if(c.length)for(let e=0;e<c.length;e++){const n=c[e];if(n){const e=()=>n(r,t);if(a)o.push(async()=>{const t=e();w(t)&&await t});else{const t=e();w(t)&&(a=!0,o.push(()=>t))}}}return}const l=t.getAttribute("q-"+n);if(l){const n=t.closest("[q\\\\:container]:not([q\\\\:container=html]):not([q\\\\:container=text])"),s=n.getAttribute("q:base"),i=new URL(s,e.baseURI),c=l.split("|"),p=C(n);for(let e=0;e<c.length;e++){const l=c[e],q=performance.now(),[u,d,f]=l.split("#"),h=e=>{if(e&&t.isConnected)try{const n=e.call(f,r,t);if(w(n))return n.catch(e=>{k("qerror",{error:e,qBase:s,symbol:d,element:t,reqTime:q})})}catch(e){k("qerror",{error:e,qBase:s,symbol:d,element:t,reqTime:q})}},b=()=>R(n,t,s,i,u,d,q),m=p&&""===u?void 0:b();if(w(m))a=!0,o.push(async()=>{p&&await p,await h(await m)});else if(a||p)a=!0,o.push(async()=>{p&&await p,await h(m||await b())});else{const e=h(m);w(e)&&(a=!0,o.push(()=>e))}}}},U=(e,t=i,r=!0)=>{const n=S(e.type),o=t+":"+n,s=c+n,a=[],l=[],p=[];let q=e.target;for(;q;)$(q)?(a.push(q),l.push(L(q,o,s)),q=q.parentElement):q=q.parentElement;for(let t=a.length-1;t>=0;t--)if(l[t]&&(x(a[t],e,o,p,n,r),e.cancelBubble||e.cancelBubble))return void E(p);for(let t=0;t<a.length;t++)if(!l[t]&&(x(a[t],e,o,p,n,r),!e.bubbles||e.cancelBubble||e.cancelBubble))return void E(p);E(p)},j=e=>U(e,a,!1),D=(e,t,r=!0)=>{const n=S(t.type),o=e+":"+n,s=m("[q-"+e+"\\\\:"+n+"]"),i=[];for(let e=0;e<s.length;e++){const a=s[e];x(a,t,o,i,n,r)}E(i)},O=e=>{D(o,e)},P=e=>{D(s,e,!1)},F=e=>{D(r,e)},J=e=>{D(n,e,!1)},M=()=>{const r=e.readyState;if("interactive"==r||"complete"==r){if(f=1,q.forEach(v),p.has("d:qinit")){p.delete("d:qinit");const e=_("qinit"),t=m("[q-d\\\\:qinit]"),r=[];for(let n=0;n<t.length;n++){const o=t[n];x(o,e,"d:qinit",r),o.removeAttribute("q-d:qinit")}E(r)}if(p.has("d:qidle")&&(p.delete("d:qidle"),(t.requestIdleCallback??t.setTimeout).bind(t)(()=>{const e=_("qidle"),t=m("[q-d\\\\:qidle]"),r=[];for(let n=0;n<t.length;n++){const o=t[n];x(o,e,"d:qidle",r),o.removeAttribute("q-d:qidle")}E(r)})),p.has("e:qvisible")){d||(d=new IntersectionObserver(e=>{const t=[];for(let r=0;r<e.length;r++){const n=e[r];n.isIntersecting&&(d.unobserve(n.target),x(n.target,_("qvisible",n),"e:qvisible",t))}E(t)}));const e=m("[q-e\\\\:qvisible]:not([q\\\\:observed])");for(let t=0;t<e.length;t++){const r=e[t];d.observe(r),r.setAttribute("q:observed","true")}}}},Z=(...e)=>{for(let n=0;n<e.length;n++){const s=e[n];if("string"==typeof s){if(!p.has(s)){p.add(s);const{scope:e,eventName:n}=I(s),i=N(e),a=T(e);a===r?g(t,n,i?J:F,!0,i):q.forEach(e=>g(e,n,a===o?i?P:O:i?j:U,!0,i)),1!==f||"e:qvisible"!==s&&"d:qinit"!==s&&"d:qidle"!==s||M()}}else q.has(s)||(p.forEach(e=>{const{scope:t,eventName:n}=I(e),i=N(t),a=T(t);a!==r&&g(s,n,a===o?i?P:O:i?j:U,!0,i)}),q.add(s))}},z=t._qwikEv;z?.roots||(Array.isArray(z)?Z(...z):Z("e:click","e:input"),t._qwikEv={events:p,roots:q,push:Z},g(e,l,M),M());"`
);
});

Expand Down
Loading