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)" />`.
|
- `@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.
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user