feat(compiler): commit deferred client state writes
This commit is contained in:
@@ -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)" />`.
|
||||
- `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.
|
||||
- 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`.
|
||||
- `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.
|
||||
|
||||
@@ -268,6 +268,13 @@ function functionEntry(
|
||||
`if (!Object.is(${name}, __wrnexusInitialState[${JSON.stringify(name)}])) context.state.${name} = ${name};`,
|
||||
)
|
||||
.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 runtimeBindings = [
|
||||
!parameterNames.has("output") ? "const output = context.output;" : "",
|
||||
@@ -281,12 +288,13 @@ function functionEntry(
|
||||
${initialStateSnapshot}
|
||||
${stateAliases}
|
||||
${propAliases}
|
||||
${commitBinding}
|
||||
${peerAliases}
|
||||
${runtimeBindings}
|
||||
try {
|
||||
${body}
|
||||
} finally {
|
||||
${copyBack}
|
||||
${stateNames.length ? "__wrnexusCommit();" : ""}
|
||||
}
|
||||
}`;
|
||||
}
|
||||
|
||||
@@ -145,3 +145,42 @@ test("browser codegen binds peer client functions through the scoped function ta
|
||||
functions.run?.();
|
||||
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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user