-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Route level async scripts can finish before hydration started, which breaks reactivity. #107
Comments
@kanashimia could you elaborate on what you mean by an It is intentional that the scripts output from Marko Vite are async. The idea here is that we kick off the loading of the Marko runtime and your component logic as soon as possible, and the Marko runtime itself ensures that your component related code is initialized as the html streams in. |
Hello @DylanPiercey basically I meant this: Script tags that are auto generated. The ordering between the execution of those scripts isn't as well defined as it should be. And the code used to test was a simple counter like this: <let /count = 0 />
<effect() {
setTimeout(() => count = count + 1, 10);
}/>
<p> Count: ${count}</p> Tested Marko 5, and it works even when the execution of the scripts is reordered, so it seems like it is a bug with Marko 6 indeed. This issue belongs to the marko repo probably, it seems script scheduling is implemented there. |
@kanashimia the inline script you pointed out is designed to work with the fact that the main runtime is loaded async/eagerly. Basically the inline runtime will tell Marko that it's ready to go (if the runtime is loaded) and when the runtime loads it checks to see if anything was done by the inline runtime. In practice what this means is that if you have an If it's not working that way then there is a bug. Could you explain a bit more what you are expecting the execution order to be and/or what specifically you see as a bug? Note Marko 5 also works like this where the runtime and the inline code co-oporate but do not block each other. |
The bug is that "it breaks reactivity" if async script is run first, for example the above counter example will stop working, its count will always stay at zero. But there are no error messages in the console, just silently breaks. This is that CLICK TO SHOW GENERATED CODE
import { e as effect, s as state, o as on, a as attr, d as data, r as register, c as createRendererWithOwner, i as init } from "./_BvIeR73t.js";
const _logoSvg = "OMITTED";
const _template_ = "OMITTED";
const _walks_ = (
/* get, next(1), over(1), replace, out(1), next(1), over(1), replace, out(1), next(2), get, out(2) */
" Db%lDb%lE m"
);
const _count2_effect = effect("e0", (_scope, {
5: count
}) => setTimeout(() => _count2(_scope, count + 1), 10));
const _count2 = /* @__PURE__ */ state(5, (_scope, count) => {
data(_scope[2], count);
_count2_effect(_scope);
});
const _count_effect = effect("e1", (_scope, {
4: count2
}) => on(_scope[0], "click", function() {
_count(_scope, console.log(count2 + 1));
}));
const _count = /* @__PURE__ */ state(4, (_scope, count2) => {
data(_scope[1], count2);
_count_effect(_scope);
});
function _setup_(_scope) {
attr(_scope[3], "src", _logoSvg);
_count(_scope, 0);
_count2(_scope, 0);
}
const _setup$Layout1_content = (_scope) => {
_setup_(_scope[0]);
};
register("l0", /* @__PURE__ */ createRendererWithOwner(
`<!>${_template_}<!>`,
/* beginChild, _Page_walks, endChild */
`D/${_walks_}&D`,
_setup$Layout1_content
));
init();
var port2 = /* @__PURE__ */ (() => {
let { port1, port2: port22 } = new MessageChannel();
return port1.onmessage = () => {
isScheduled = false, run();
}, port22;
})(), isScheduled;
function schedule() {
isScheduled || (isScheduled = true, queueMicrotask(flushAndWaitFrame));
}
function flushAndWaitFrame() {
run(), requestAnimationFrame(triggerMacroTask);
}
function triggerMacroTask() {
port2.postMessage(0);
}
var DEFAULT_RUNTIME_ID = "M";
function onDestroy(scope) {
let parentScope = scope.d;
for (; parentScope && !parentScope.h?.has(scope); )
(parentScope.h ||= /* @__PURE__ */ new Set()).add(scope), scope = parentScope, parentScope = scope.d;
}
var registeredValues = {}, Render = class {
m = [];
n = {};
y = {
_: registeredValues
};
constructor(renders, runtimeId, renderId) {
this.z = renders, this.A = runtimeId, this.o = renderId, this.p = renders[renderId], this.q();
}
w() {
this.p.w(), this.q();
}
q() {
let data2 = this.p, serializeContext = this.y, scopeLookup = this.n, visits = data2.v, cleanupOwners = /* @__PURE__ */ new Map();
if (visits.length) {
let commentPrefixLen = data2.i.length, cleanupMarkers = /* @__PURE__ */ new Map();
data2.v = [];
let sectionEnd = (visit, scopeId = this.f, curNode = visit) => {
let scope = scopeLookup[scopeId] ||= {}, endNode = curNode;
for (; (endNode = endNode.previousSibling).nodeType === 8; ) ;
scope.b = endNode;
let startNode = scope.a ||= endNode, len = cleanupMarkers.size;
for (let [markerScopeId, markerNode] of cleanupMarkers) {
if (!len--) break;
markerScopeId !== scopeId && startNode.compareDocumentPosition(markerNode) & 4 && curNode.compareDocumentPosition(markerNode) & 2 && (cleanupOwners.set("" + markerScopeId, scopeId), cleanupMarkers.delete(markerScopeId));
}
return cleanupMarkers.set(scopeId, visit), scope;
};
for (let visit of visits) {
let commentText = visit.data, token = commentText[commentPrefixLen], scopeId = parseInt(commentText.slice(commentPrefixLen + 1)), scope = scopeLookup[scopeId] ||= {}, dataIndex = commentText.indexOf(" ") + 1, data3 = dataIndex ? commentText.slice(dataIndex) : "";
if (token === "*")
scope[data3] = visit.previousSibling;
else if (token === "$")
cleanupMarkers.set(scopeId, visit);
else if (token === "[")
this.f && (data3 && sectionEnd(visit), this.m.push(this.f)), this.f = scopeId, scope.a = visit;
else if (token === "]") {
if (scope[data3] = visit, scopeId < this.f) {
let currParent = visit.parentNode, startNode = sectionEnd(visit).a;
currParent && currParent !== startNode.parentNode && currParent.prepend(startNode), this.f = this.m.pop();
}
} else if (token === "|") {
scope[parseInt(data3)] = visit;
let childScopeIds = JSON.parse(
"[" + data3.slice(data3.indexOf(" ") + 1) + "]"
), curNode = visit;
for (let i = childScopeIds.length - 1; i >= 0; i--)
curNode = sectionEnd(visit, childScopeIds[i], curNode).b;
}
}
}
let resumes = data2.r;
if (resumes) {
data2.r = [];
let len = resumes.length, i = 0;
try {
for (isResuming = true; i < len; ) {
let resumeData = resumes[i++];
if (typeof resumeData == "function") {
let scopes = resumeData(serializeContext), { $global } = scopeLookup;
$global || (scopeLookup.$global = $global = scopes.$ || {}, $global.runtimeId = this.A, $global.renderId = this.o);
for (let scopeId in scopes)
if (scopeId !== "$") {
let scope = scopes[scopeId], prevScope = scopeLookup[scopeId];
scope.$global = $global, prevScope !== scope && (scopeLookup[scopeId] = Object.assign(
scope,
prevScope
));
let cleanupOwnerId = cleanupOwners.get(scopeId);
cleanupOwnerId && (scope.d = scopes[cleanupOwnerId], onDestroy(scope));
}
} else i === len || typeof resumes[i] != "string" ? delete this.z[this.o] : registeredValues[resumes[i++]](
scopeLookup[resumeData],
scopeLookup[resumeData]
);
}
} finally {
isResuming = false;
}
}
}
}, isResuming = false;
function register(id, obj) {
return registeredValues[id] = obj, obj;
}
function init(runtimeId = DEFAULT_RUNTIME_ID) {
let resumeRender = (renderId) => resumeRender[renderId] = renders[renderId] = new Render(renders, runtimeId, renderId), renders;
window[runtimeId] ? setRenders(window[runtimeId]) : Object.defineProperty(window, runtimeId, {
configurable: true,
set: setRenders
});
function setRenders(v) {
renders = v;
for (let renderId in v)
resumeRender(renderId);
Object.defineProperty(window, runtimeId, {
configurable: true,
value: resumeRender
});
}
}
var MARK = {}, CLEAN = {}, DIRTY = {};
function state(valueAccessor, fn, getIntersection) {
let valueSignal = value(valueAccessor, fn, getIntersection), markAccessor = valueAccessor + "#", valueChangeAccessor = valueAccessor + "@";
return (scope, valueOrOp, valueChange) => (rendering ? valueSignal(
scope,
valueOrOp === MARK || valueOrOp === CLEAN || valueOrOp === DIRTY || (scope[valueChangeAccessor] = valueChange) || scope[markAccessor] === void 0 ? valueOrOp : CLEAN
) : scope[valueChangeAccessor] ? scope[valueChangeAccessor](valueOrOp) : queueSource(scope, valueSignal, valueOrOp), valueOrOp);
}
function value(valueAccessor, fn, getIntersection) {
let markAccessor = valueAccessor + "#", intersection2 = getIntersection && ((scope, op) => (intersection2 = getIntersection())(scope, op));
return (scope, valueOrOp) => {
if (valueOrOp === MARK)
(scope[markAccessor] = (scope[markAccessor] ?? 0) + 1) === 1 && intersection2?.(scope, MARK);
else if (valueOrOp !== DIRTY) {
let existing = scope[markAccessor] !== void 0;
(scope[markAccessor] ||= 1) === 1 && (valueOrOp === CLEAN || existing && scope[valueAccessor] === valueOrOp ? intersection2?.(scope, CLEAN) : (scope[valueAccessor] = valueOrOp, fn && fn(scope, valueOrOp), intersection2?.(scope, DIRTY))), scope[markAccessor]--;
}
};
}
function effect(id, fn) {
return register(id, fn), (scope) => {
queueEffect(scope, fn);
};
}
var pendingSignals = [], pendingEffects = [], rendering = false;
function queueSource(scope, signal, value2) {
return schedule(), rendering = true, signal(scope, MARK), rendering = false, pendingSignals.push(scope, signal, value2), value2;
}
function queueEffect(scope, fn) {
pendingEffects.push(scope, fn);
}
function run() {
let signals = pendingSignals, effects = pendingEffects;
try {
rendering = true, pendingSignals = [], runSignals(signals);
} finally {
rendering = false;
}
pendingEffects = [], runEffects(effects);
}
function runEffects(effects = pendingEffects) {
for (let i = 0; i < effects.length; i += 2) {
let scope = effects[i], fn = effects[i + 1];
fn(scope, scope);
}
}
function runSignals(signals) {
for (let i = 0; i < signals.length; i += 3) {
let scope = signals[
i + 0
/* Scope */
], signal = signals[
i + 1
/* Signal */
], value2 = signals[
i + 2
/* Value */
];
signal(scope, value2);
}
}
var elementHandlersByEvent = /* @__PURE__ */ new Map(), defaultDelegator = createDelegator();
function on(element, type, handler) {
let handlersByElement = elementHandlersByEvent.get(type);
handlersByElement || elementHandlersByEvent.set(type, handlersByElement = /* @__PURE__ */ new WeakMap()), handlersByElement.has(element) || defaultDelegator(element, type, handleDelegated), handlersByElement.set(element, handler);
}
function createDelegator() {
let delegatedEventsByRoot = /* @__PURE__ */ new WeakMap();
return function(node, type, handler) {
let root = node.getRootNode(), delegatedEvents = delegatedEventsByRoot.get(root);
delegatedEvents || delegatedEventsByRoot.set(root, delegatedEvents = /* @__PURE__ */ new Set()), delegatedEvents.has(type) || (delegatedEvents.add(type), root.addEventListener(type, handler, true));
};
}
function handleDelegated(ev) {
let target = ev.target;
if (target) {
let handlersByElement = elementHandlersByEvent.get(ev.type);
if (handlersByElement.get(target)?.(ev, target), ev.bubbles)
for (; (target = target.parentElement) && !ev.cancelBubble; )
handlersByElement.get(target)?.(ev, target);
}
}
var fallback = /* @__PURE__ */ document.createTextNode(""), parser = /* @__PURE__ */ new Range();
function parseHTML(html2) {
return parser.createContextualFragment(html2);
}
function parseHTMLOrSingleNode(html2) {
let content = parseHTML(html2);
return content.firstChild ? content.firstChild === content.lastChild && // If the firstChild is a comment it's possible its
// a single replaced node, in which case the walker can't replace
// the node itself.
content.firstChild.nodeType !== 8 ? content.firstChild : content : fallback;
}
function attr(element, name, value2) {
setAttribute(element, name, normalizeAttrValue(value2));
}
function setAttribute(element, name, value2) {
element.getAttribute(name) != value2 && (value2 === void 0 ? element.removeAttribute(name) : element.setAttribute(name, value2));
}
function data(node, value2) {
let normalizedValue = normalizeString(value2);
node.data !== normalizedValue && (node.data = normalizedValue);
}
function normalizeAttrValue(value2) {
if (value2 || value2 === 0)
return value2 === true ? "" : value2 + "";
}
function normalizeString(value2) {
return value2 || value2 === 0 ? value2 + "" : "";
}
function trimWalkString(walkString) {
let end = walkString.length;
for (; walkString.charCodeAt(--end) > 47; ) ;
return walkString.slice(0, end + 1);
}
function createRendererWithOwner(template, rawWalks, setup, getClosureSignals, getArgs) {
let args, closureSignals, id = {}, walks = rawWalks ? /* @__PURE__ */ trimWalkString(rawWalks) : " ";
return (owner) => ({
t: id,
D: template,
C: walks,
s: setup,
k: _clone,
B: owner,
E: void 0,
get e() {
return args ||= getArgs?.();
},
get c() {
return closureSignals ||= new Set(getClosureSignals?.());
}
});
}
function _clone() {
return (this.E ||= parseHTMLOrSingleNode(
this.D
)).cloneNode(true);
}
export {
attr as a,
createRendererWithOwner as c,
data as d,
effect as e,
init as i,
on as o,
register as r,
state as s
}; I can try to provide test case for marko to test against ordering if really needed. |
Inline non module script tag at the end of the body can execute after the async script tag at the header.
As far as I know there are no ordering guarantees with async scripts, so this should be guarded against somehow, route scripts should wait for hydration to start.
For me it only breaks very rarely when navigating through pages pretty fast.
You can easily check that ordering matters when you change both scripts to
type="module"
and removeasync
.Package info:
@marko/[email protected]
[email protected]
May be related to Marko 6, haven't checked if it happens on previous version.
Actually looking at it I think this is may be a bug in the vite plugin instead.
The text was updated successfully, but these errors were encountered: