feat(compiler): commit deferred client state writes

This commit is contained in:
2026-08-09 13:21:34 +05:30
parent 3e77a621f5
commit b2e83bc941
3 changed files with 49 additions and 1 deletions
+1
View File
@@ -201,6 +201,7 @@ A file opens with `page <Name>` or `component <Name>` followed by a `{ ... }` bo
- `@event name = function` inside `props` — declares a public component event. Emit it from component behavior with `name(detail)` or `$emit("name", detail)`, and consume it with `<Component @name="handler(event)" />`. - `@event name = function` inside `props` — declares a public component event. Emit it from component behavior with `name(detail)` or `$emit("name", detail)`, and consume it with `<Component @name="handler(event)" />`.
- `state <ident>: Type = <expr>` — typed reactive state seeded from a raw JS expression, including native array and object literals. The annotation is optional for backward compatibility. - `state <ident>: Type = <expr>` — typed reactive state seeded from a raw JS expression, including native array and object literals. The annotation is optional for backward compatibility.
- `view { <html> }` — plain HTML with `{expr}` interpolation in text and attributes, JSX-style component props such as `items={items}`, `items={[...]}`, and `options={{...}}`, hyphenated attributes, boolean attributes, `@event="..."` client bindings, and `<!-- comments -->`. Structured component props are serialized safely for SSR; expressions that reference `state` retain their initial value and update reactively in the browser. - `view { <html> }` — plain HTML with `{expr}` interpolation in text and attributes, JSX-style component props such as `items={items}`, `items={[...]}`, and `options={{...}}`, hyphenated attributes, boolean attributes, `@event="..."` client bindings, and `<!-- comments -->`. Structured component props are serialized safely for SSR; expressions that reference `state` retain their initial value and update reactively in the browser.
- Client functions automatically commit state changed by `setTimeout` callbacks. For other deferred callbacks (observers, third-party APIs, or detached promise callbacks), call the injected `commit()` function after changing local state; returning/awaiting a promise also commits through the normal function boundary.
- `seo { key = "value" ... }` — metadata merged into the generated `meta`. - `seo { key = "value" ... }` — metadata merged into the generated `meta`.
- `style { <raw css> }` — inlined page/component stylesheet (repeatable). - `style { <raw css> }` — inlined page/component stylesheet (repeatable).
- `functions { <TypeScript> }` — helpers with typed parameters and return values. Types remain in server output and are safely erased from browser behavior code. - `functions { <TypeScript> }` — helpers with typed parameters and return values. Types remain in server output and are safely erased from browser behavior code.
+9 -1
View File
@@ -268,6 +268,13 @@ function functionEntry(
`if (!Object.is(${name}, __wrnexusInitialState[${JSON.stringify(name)}])) context.state.${name} = ${name};`, `if (!Object.is(${name}, __wrnexusInitialState[${JSON.stringify(name)}])) context.state.${name} = ${name};`,
) )
.join("\n"); .join("\n");
const commitBinding = stateNames.length
? `const __wrnexusCommit = () => { ${copyBack} };
${!parameterNames.has("commit") && !declaredLocals.has("commit") ? "const commit = __wrnexusCommit;" : ""}
${!parameterNames.has("setTimeout") && !declaredLocals.has("setTimeout") ? `const setTimeout = (callback, delay, ...args) => globalThis.setTimeout(() => {
try { return callback(...args); } finally { __wrnexusCommit(); }
}, delay);` : ""}`
: "";
const body = eraseFunctionTypes(fn.body); const body = eraseFunctionTypes(fn.body);
const runtimeBindings = [ const runtimeBindings = [
!parameterNames.has("output") ? "const output = context.output;" : "", !parameterNames.has("output") ? "const output = context.output;" : "",
@@ -281,12 +288,13 @@ function functionEntry(
${initialStateSnapshot} ${initialStateSnapshot}
${stateAliases} ${stateAliases}
${propAliases} ${propAliases}
${commitBinding}
${peerAliases} ${peerAliases}
${runtimeBindings} ${runtimeBindings}
try { try {
${body} ${body}
} finally { } finally {
${copyBack} ${stateNames.length ? "__wrnexusCommit();" : ""}
} }
}`; }`;
} }
@@ -145,3 +145,42 @@ test("browser codegen binds peer client functions through the scoped function ta
functions.run?.(); functions.run?.();
expect(state.value).toBe(1); expect(state.value).toBe(1);
}); });
test("deferred timer state writes commit after the client function returns", async () => {
const targets = generateTargets(
parse(`component DeferredWrite {
state { value: number = 0 }
functions {
client function later(): void { setTimeout(() => { value = 2 }, 0) }
}
view { <button @click='later()'>{value}</button> }
}`),
);
const executable = new Function(
`${targets.browser.replace(/^export\s+/gm, "")}\nreturn { bindClientScope };`,
)() as { bindClientScope: (context: Record<string, unknown>) => Record<string, () => void> };
let rendered = "0";
const state = new Proxy(
{ value: 0 },
{
set(target, property, value) {
Reflect.set(target, property, value);
rendered = String(value);
return true;
},
},
);
const functions = executable.bindClientScope({
state,
props: {},
output: {},
server: {},
refs: {},
});
functions.later?.();
expect(state.value).toBe(0);
await new Promise((resolve) => setTimeout(resolve, 10));
expect(state.value).toBe(2);
expect(rendered).toBe("2");
});