diff --git a/packages/compiler/src/client-codegen.ts b/packages/compiler/src/client-codegen.ts
index a3094fba..a838719b 100644
--- a/packages/compiler/src/client-codegen.ts
+++ b/packages/compiler/src/client-codegen.ts
@@ -308,21 +308,82 @@ export function generateBrowserModule(ast: PageAst): string {
const selectedImports = selectedBrowserImports(ast, functions);
const imports = selectedImports.map((entry) => entry.code).join("\n");
const importedBindings = [...new Set(selectedImports.flatMap((entry) => entry.bindings))];
+ const sharedState = state.filter((name) => safeIdentifier(name) && !RUNTIME_BINDINGS.has(name));
+ const sharedProps = ast.props
+ .map((entry) => entry.name)
+ .filter(
+ (name) =>
+ safeIdentifier(name) &&
+ !RUNTIME_BINDINGS.has(name) &&
+ !sharedState.includes(name),
+ );
+ const callableAliases = functionNames.filter(
+ (name) =>
+ safeIdentifier(name) &&
+ !RUNTIME_BINDINGS.has(name) &&
+ !sharedState.includes(name) &&
+ !sharedProps.includes(name),
+ );
+ const sharedCommit = sharedState.map((name) => `context.state.${name} = ${name};`).join(" ");
+ const sharedRestore = [
+ ...sharedState.map((name) => `${name} = context.state.${name};`),
+ ...sharedProps.map((name) => `${name} = context.props.${name};`),
+ ].join(" ");
+ const hasAuthoredCommit = callableAliases.includes("commit");
+ const hasAuthoredSetTimeout = callableAliases.includes("setTimeout");
+ const implementations = functions
+ .map((fn) => {
+ const parameters = fn.parameters.map((parameter) => parameter.name).join(", ");
+ return `${JSON.stringify(fn.name)}: ${fn.async ? "async " : ""}function(${parameters}) {
+ try { ${eraseFunctionTypes(fn.body)} } finally { __wrnexusCommit(); }
+ }`;
+ })
+ .join(",\n");
return `// generated WRNexusJS browser module for ${ast.name}
${imports}
export const __wrnexusClientFunctions = {
-${functions.map((fn) => ` ${functionEntry(ast, fn, functionNames)}`).join(",\n")}
+${functions
+ .map(
+ (fn) =>
+ ` ${JSON.stringify(fn.name)}: (context, ...args) => __wrnexusBindings(context)[${JSON.stringify(fn.name)}](...args)`,
+ )
+ .join(",\n")}
};
export const __wrnexusClientState = ${JSON.stringify(state)};
export const __wrnexusOutputs = ${JSON.stringify(ast.outputs)};
export const __wrnexusImportedBindings = { ${importedBindings.join(", ")} };
-export function bindClientScope(context) {
+function __wrnexusCreateClientFunctions(context) {
+ ${sharedState.length ? `let { ${sharedState.join(", ")} } = context.state;` : ""}
+ ${sharedProps.length ? `let { ${sharedProps.join(", ")} } = context.props;` : ""}
+ const output = context.output;
+ const server = context.server;
+ const props = context.props;
+ const refs = context.refs;
+ const __wrnexusCommit = () => { ${sharedCommit} };
+ const __wrnexusRestore = () => { ${sharedRestore} };
+ ${!hasAuthoredCommit ? "const commit = __wrnexusCommit;" : ""}
+ ${!hasAuthoredSetTimeout ? `const setTimeout = (callback, delay, ...args) => globalThis.setTimeout(() => {
+ try { return callback(...args); } finally { __wrnexusCommit(); }
+ }, delay);` : ""}
+ const implementations = {
+ ${implementations}
+ };
+ ${callableAliases.map((name) => `const ${name} = (...args) => implementations[${JSON.stringify(name)}](...args);`).join("\n ")}
const functions = {};
- const scopedContext = { ...context, functions };
- for (const [name, handler] of Object.entries(__wrnexusClientFunctions)) {
- functions[name] = (...args) => handler(scopedContext, ...args);
+ for (const name of Object.keys(implementations)) {
+ functions[name] = (...args) => {
+ __wrnexusRestore();
+ return implementations[name](...args);
+ };
}
return functions;
}
+function __wrnexusBindings(context) {
+ return context.__wrnexusBoundFunctions ||
+ (context.__wrnexusBoundFunctions = __wrnexusCreateClientFunctions(context));
+}
+export function bindClientScope(context) {
+ return __wrnexusBindings(context);
+}
`;
}
diff --git a/packages/compiler/test/v060-targets.test.ts b/packages/compiler/test/v060-targets.test.ts
index 18749762..2fc33a20 100644
--- a/packages/compiler/test/v060-targets.test.ts
+++ b/packages/compiler/test/v060-targets.test.ts
@@ -1,4 +1,5 @@
import { expect, test } from "bun:test";
+import { gzipSync } from "bun";
import { generateTargets, parse } from "../src/index.ts";
const ast = parse(`component ConfirmDialog {
@@ -130,7 +131,7 @@ test("browser codegen binds peer client functions through the scoped function ta
view { }
}`),
);
- expect(targets.browser).toContain("context.functions");
+ expect(targets.browser).toContain('implementations["increment"]');
const executable = new Function(
`${targets.browser.replace(/^export\s+/gm, "")}\nreturn { bindClientScope };`,
)() as { bindClientScope: (context: Record) => Record void> };
@@ -200,3 +201,27 @@ test("the deferred commit helper does not shadow an authored commit function", (
expect(() => new Function(targets.browser.replace(/^export\s+/gm, ""))).not.toThrow();
expect(targets.browser).not.toContain("const commit = __wrnexusCommit");
});
+
+test("generated client modules stay below decoded-size and duplication ratchets", () => {
+ const states = Array.from({ length: 12 }, (_, index) => `s${index}: number = ${index}`).join(" ");
+ const functions = Array.from(
+ { length: 20 },
+ (_, index) =>
+ `client function f${index}(): void { s${index % 12} += 1 ${Array.from(
+ { length: 20 },
+ (_unused, peer) => (peer === index ? "" : `if (false) f${peer}()`),
+ ).join(" ")} }`,
+ ).join("\n");
+ const browser = generateTargets(
+ parse(`component ClientSizeStress {
+ state { ${states} }
+ functions { ${functions} }
+ view { }
+ }`),
+ ).browser;
+ const decoded = new TextEncoder().encode(browser).length;
+ const compressed = gzipSync(new TextEncoder().encode(browser)).length;
+
+ expect(decoded).toBeLessThanOrEqual(13_000);
+ expect(decoded / compressed).toBeLessThanOrEqual(10);
+});