import { eraseFunctionTypes, type PageAst, type RuntimeFunctionDecl } from "@wrnexus/syntax"; import { generateDeclarations } from "./type-codegen.ts"; import { rpcManifest } from "./server-codegen.ts"; const RESERVED_BINDINGS = new Set([ "await", "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do", "else", "enum", "export", "extends", "false", "finally", "for", "function", "if", "implements", "import", "in", "instanceof", "interface", "let", "new", "null", "package", "private", "protected", "public", "return", "static", "super", "switch", "this", "throw", "true", "try", "typeof", "var", "void", "while", "with", "yield", ]); function safeBinding(name: string): boolean { return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !RESERVED_BINDINGS.has(name); } function stateObject(ast: PageAst, runtime: "shared" | "client" | "server"): string { const entries = ast.states .filter((state) => state.runtime === runtime) .map((state) => `${JSON.stringify(state.name)}: (${state.expr})`); return `{ ${entries.join(", ")} }`; } function actionSource(fn: RuntimeFunctionDecl, stateNames: string[], eraseTypes = false): string { const parameterNames = new Set(fn.parameters.map((param) => param.name)); const params = fn.parameters.map((param) => param.name).join(", "); const aliases = stateNames.filter((name) => safeBinding(name) && !parameterNames.has(name)); const aliasSource = aliases.length ? `let { ${aliases.join(", ")} } = context.state;` : ""; const runtimeAliases = ["server"] .filter((name) => !parameterNames.has(name)) .map((name) => `const ${name} = context.${name};`) .join("\n"); const copyBack = aliases.map((name) => `context.state.${name} = ${name};`).join("\n"); const body = eraseTypes ? eraseFunctionTypes(fn.body) : fn.body; return `{ runtime: ${JSON.stringify(fn.runtime)}, handler: ${fn.async ? "async " : ""}function(context${params ? `, ${params}` : ""}) { ${runtimeAliases}\n${aliasSource}\ntry { ${body} } finally { ${copyBack} } } }`; } function persistedCallback( source: string | undefined, functionName: "migrate" | "validate", ): string | undefined { if (!source?.trim()) return undefined; const body = eraseFunctionTypes(source); if (functionName === "migrate") { return `(value, fromVersion, toVersion) => {\n${body}\nif (typeof migrate === "function") return migrate(value, fromVersion, toVersion);\nreturn value;\n}`; } return `(value) => {\n${body}\nif (typeof validate === "function") return validate(value);\nreturn value && typeof value === "object" && !Array.isArray(value) ? value : null;\n}`; } function persistenceSource(ast: PageAst): string { if (!ast.persist) return "undefined"; const migrate = persistedCallback(ast.persist.migrations, "migrate"); const validate = persistedCallback(ast.persist.validation, "validate"); return `{ storage: ${JSON.stringify(ast.persist.storage)}, include: ${JSON.stringify(ast.persist.include)}, version: ${ast.persist.version}${migrate ? `, migrate: ${migrate}` : ""}${validate ? `, validate: ${validate}` : ""} }`; } function lifecycleSource(ast: PageAst, stateNames: string[], browser: boolean): string { return Object.entries(ast.storeLifecycle) .filter(([name]) => !browser || name !== "serverInit") .map(([name, body]) => { const aliases = stateNames.filter(safeBinding); const aliasSource = aliases.length ? `let { ${aliases.join(", ")} } = context.state;` : ""; const runtimeAliases = browser ? "const server = context.server;" : ""; const copyBack = aliases.map((state) => `context.state.${state} = ${state};`).join("\n"); const emittedBody = browser ? eraseFunctionTypes(body) : body; return `${name}: async (context) => { ${runtimeAliases} ${aliasSource} try { ${emittedBody} } finally { ${copyBack} } }`; }) .join(",\n"); } export function generateStoreModule(ast: PageAst): string { if (ast.kind !== "global-store" && ast.kind !== "page-store") { throw new Error("generateStoreModule requires a store AST"); } const stateNames = ast.states.map((state) => state.name); const safeStateNames = stateNames.filter(safeBinding); const computed = ast.computed .map( (entry) => `${JSON.stringify(entry.name)}: (state) => { ${safeStateNames.length ? `const { ${safeStateNames.join(", ")} } = state;` : ""} return (${entry.expr}); }`, ) .join(",\n"); const actionGroups = new Map(); for (const fn of ast.runtimeFunctions) { const group = actionGroups.get(fn.name) ?? []; group.push(fn); actionGroups.set(fn.name, group); } const actions = Array.from( actionGroups, ([name, functions]) => `${JSON.stringify(name)}: [${functions.map((fn) => actionSource(fn, stateNames)).join(", ")}]`, ).join(",\n"); const persistence = persistenceSource(ast); const lifecycle = lifecycleSource(ast, stateNames, false); const manifest = rpcManifest(ast); const remoteFunctions = manifest.map((entry) => entry.function); const rpcWrappers = remoteFunctions .map( (name) => `${JSON.stringify(name)}: async (...received) => { const rpcContext = received.pop(); if (!rpcContext || !rpcContext.request) throw new Error("WRN-RPC-CONTEXT: request context is required"); let container = __wrnexusRpcContainers.get(rpcContext.request); if (!container) { const url = new URL(rpcContext.request.url); container = createRequestStoreContainer(rpcContext.request, url.pathname + url.search); __wrnexusRpcContainers.set(rpcContext.request, container); } const store = await container.use(${ast.name}); const action = store.actions[${JSON.stringify(name)}]; if (typeof action !== "function") throw new Error(${JSON.stringify(`WRN-RPC-FUNCTION: store action '${name}' is unavailable on the server`)}); return action(...received); }`, ) .join(",\n"); return `${ast.imports.join("\n")}\nimport { defineStore } from "@wrnexus/store";\nimport { createRequestStoreContainer } from "@wrnexus/store/server";\n\n${ast.types.join("\n\n")}\n\nexport const ${ast.name} = defineStore({\n name: ${JSON.stringify(ast.name)},\n kind: ${JSON.stringify(ast.storeKind)},\n createSharedState: () => (${stateObject(ast, "shared")}),\n createClientState: () => (${stateObject(ast, "client")}),\n createServerState: () => (${stateObject(ast, "server")}),\n computed: { ${computed} },\n actions: { ${actions} },\n persist: ${persistence},\n lifecycle: { ${lifecycle} },\n});\n\nexport default ${ast.name};\n\nconst __wrnexusRpcContainers = new WeakMap();\nexport const __wrnexusServerFunctions = {\n${rpcWrappers}\n};\nexport const __wrnexusRpcManifest = ${JSON.stringify(manifest, null, 2)};\n\n${generateDeclarations(ast)}\n`; } /** Standalone browser artifact for an imported `.wrn` store. */ export function generateStoreBrowserModule(ast: PageAst): string { if (ast.kind !== "global-store" && ast.kind !== "page-store") { throw new Error("generateStoreBrowserModule requires a store AST"); } const browserStates = ast.states.filter((state) => state.runtime !== "server"); const stateNames = browserStates.map((state) => state.name); const safeStateNames = stateNames.filter(safeBinding); const initialState = `{ ${browserStates.map((state) => `${JSON.stringify(state.name)}: (${state.expr})`).join(", ")} }`; const computed = ast.computed .map( (entry) => `${JSON.stringify(entry.name)}: (state) => { ${safeStateNames.length ? `const { ${safeStateNames.join(", ")} } = state;` : ""} return (${entry.expr}); }`, ) .join(",\n"); const groups = new Map(); for (const fn of ast.runtimeFunctions.filter((entry) => ["client", "shared", "legacy"].includes(entry.runtime), )) { const group = groups.get(fn.name) ?? []; group.push(fn); groups.set(fn.name, group); } const actions = Array.from( groups, ([name, functions]) => `${JSON.stringify(name)}: [${functions.map((fn) => actionSource(fn, stateNames, true)).join(", ")}]`, ).join(",\n"); const persistence = persistenceSource(ast); const lifecycleEntries = lifecycleSource(ast, stateNames, true); return `// generated WRNexusJS browser store module for ${ast.name} const __root = globalThis; const __registry = __root.__wrnexusStoreRegistry || (__root.__wrnexusStoreRegistry = new Map()); const __hydrationNode = typeof document !== "undefined" ? document.querySelector("script[data-wrnexus-store-hydration]") : null; let __hydration = {}; try { __hydration = __hydrationNode ? JSON.parse(__hydrationNode.textContent || "{}") : {}; } catch (_) {} function __clone(value) { try { return structuredClone(value); } catch (_) { return JSON.parse(JSON.stringify(value)); } } function __storage(kind) { if (typeof window === "undefined") return null; return kind === "local" ? window.localStorage : kind === "session" ? window.sessionStorage : null; } function __diagnostic(code, message, details) { const detail = { code, message, store: ${JSON.stringify(ast.name)}, details }; try { __root.dispatchEvent && __root.dispatchEvent(new CustomEvent("wrnexus:store-diagnostic", { detail })); } catch (_) {} if (typeof console !== "undefined" && console.warn) console.warn("[wrnexus:store] " + code + ": " + message, details || ""); } function __csrfToken() { if (typeof document === "undefined") return undefined; const match = /(?:^|;\\s*)wire-csrf=([^;]+)/.exec(document.cookie || ""); return match ? decodeURIComponent(match[1]) : undefined; } async function __callServerFunction(storeName, functionName, args, options) { options = options || {}; const csrf = options.csrfToken || __csrfToken(); const traceId = options.traceId || (globalThis.crypto && crypto.randomUUID ? crypto.randomUUID() : String(Date.now())); const response = await fetch(options.endpoint || "/__wrnexus/rpc", { method: "POST", credentials: "same-origin", signal: options.signal, headers: Object.assign({ "content-type": "application/json", "x-request-id": traceId }, csrf ? { "x-csrf-token": csrf } : {}, options.headers || {}), body: JSON.stringify({ component: storeName, function: functionName, args: args }), }); const payload = await response.json().catch(function () { return null; }); if (!response.ok || !payload || !payload.ok) { const error = new Error(payload && payload.error && payload.error.message || "Server call failed (" + response.status + ")"); error.code = payload && payload.error && payload.error.code || "WRN-RPC-FAILED"; error.status = response.status; error.details = payload && payload.error && payload.error.details; error.traceId = payload && payload.error && payload.error.traceId || traceId; throw error; } return payload.value; } function __compatible(expected, value) { if (expected === null || value === null) return expected === value || expected === null; if (Array.isArray(expected)) return Array.isArray(value); return typeof expected === typeof value; } function __create(definition) { const routeId = typeof location !== "undefined" ? location.pathname + location.search : "default"; const key = definition.kind === "page" ? definition.name + "@" + routeId : definition.name; if (__registry.has(key)) return __registry.get(key); let currentDefinition = definition; const listeners = new Set(); const initial = currentDefinition.createState(); let restored = null; if (currentDefinition.persist) { try { const storage = __storage(currentDefinition.persist.storage); const rawValue = storage && storage.getItem("wrnexus:store:" + currentDefinition.name); const parsed = rawValue ? JSON.parse(rawValue) : null; if (parsed) { let candidate = parsed.state; const fromVersion = Number(parsed.version || 0); if (fromVersion !== currentDefinition.persist.version) { if (typeof currentDefinition.persist.migrate === "function") candidate = currentDefinition.persist.migrate(candidate, fromVersion, currentDefinition.persist.version); else { __diagnostic("WRN-PERSIST-VERSION", "Persisted state version cannot be restored without a migration.", { fromVersion, toVersion: currentDefinition.persist.version }); candidate = null; } } if (candidate && typeof currentDefinition.persist.validate === "function") candidate = currentDefinition.persist.validate(candidate); if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) { restored = {}; currentDefinition.persist.include.forEach(function (name) { if (Object.prototype.hasOwnProperty.call(candidate, name)) restored[name] = candidate[name]; }); } else if (candidate != null) { __diagnostic("WRN-PERSIST-INVALID", "Persisted state failed validation and was reset.", candidate); } } } catch (error) { __diagnostic("WRN-PERSIST-RESTORE", "Persisted state could not be restored and was reset.", error); } } const raw = Object.assign({}, initial, restored || {}, __hydration[currentDefinition.name] || {}); let mutable = false; let actionName = "direct"; function persistState() { if (!currentDefinition.persist) return; try { const picked = {}; currentDefinition.persist.include.forEach(function (name) { picked[name] = raw[name]; }); const storage = __storage(currentDefinition.persist.storage); if (storage) storage.setItem("wrnexus:store:" + currentDefinition.name, JSON.stringify({ version: currentDefinition.persist.version, state: picked })); } catch (error) { __diagnostic("WRN-PERSIST-WRITE", "Persisted state could not be written.", error); } } const state = new Proxy(raw, { set(target, property, value) { if (!mutable) throw new TypeError("WRN-STORE-READONLY: " + currentDefinition.name + "." + String(property) + " must be changed by a store action."); if (Object.is(target[property], value)) return true; target[property] = value; persistState(); listeners.forEach(function (listener) { listener(instance.snapshot(), { store: currentDefinition.name, action: actionName, changed: [String(property)] }); }); return true; }, deleteProperty(target, property) { if (!mutable) throw new TypeError("WRN-STORE-READONLY: store state is readonly outside actions"); return Reflect.deleteProperty(target, property); }, }); const actions = {}; const server = new Proxy({}, { get: function (_target, property) { return function () { return __callServerFunction(currentDefinition.name, String(property), Array.prototype.slice.call(arguments)); }; } }); function installActions() { Object.keys(actions).forEach(function (name) { delete actions[name]; }); Object.entries(currentDefinition.actions || {}).forEach(function (pair) { const name = pair[0], candidates = pair[1]; const selected = candidates.find(function (entry) { return entry.runtime === "client"; }) || candidates.find(function (entry) { return entry.runtime === "shared"; }) || candidates.find(function (entry) { return entry.runtime === "legacy"; }); if (!selected) return; actions[name] = async function () { const args = Array.prototype.slice.call(arguments); const previousMutable = mutable, previousAction = actionName; mutable = true; actionName = name; try { return await selected.handler({ state, snapshot: function () { return __clone(state); }, reset: function () { return instance.reset(); }, runtime: "client", routeId, server }, ...args); } finally { mutable = previousMutable; actionName = previousAction; } }; }); } installActions(); const core = { name: currentDefinition.name, kind: currentDefinition.kind, state, actions, whenReady: Promise.resolve(), reset() { mutable = true; actionName = "$reset"; try { const next = currentDefinition.createState(); Object.keys(raw).forEach(function (name) { if (!(name in next)) delete raw[name]; }); Object.assign(raw, next); persistState(); listeners.forEach(function (listener) { listener(instance.snapshot(), { store: currentDefinition.name, action: "$reset", changed: Object.keys(next) }); }); } finally { mutable = false; actionName = "direct"; } }, snapshot() { return Object.freeze(__clone(raw)); }, subscribe(listener) { listeners.add(listener); return function () { listeners.delete(listener); }; }, async dispose() { mutable = true; actionName = "$dispose"; try { await currentDefinition.lifecycle && currentDefinition.lifecycle.dispose && currentDefinition.lifecycle.dispose({ state, runtime: "client", routeId, server }); } finally { mutable = false; actionName = "direct"; listeners.clear(); __registry.delete(key); } }, async __hotUpdate(nextDefinition) { const previous = __clone(raw); const nextShape = nextDefinition.createState(); const preserved = [], reset = [], added = [], removed = []; Object.keys(previous).forEach(function (name) { if (!(name in nextShape)) { removed.push(name); return; } if (__compatible(nextShape[name], previous[name])) { nextShape[name] = previous[name]; preserved.push(name); } else reset.push(name); }); Object.keys(nextShape).forEach(function (name) { if (!(name in previous)) added.push(name); }); currentDefinition = nextDefinition; mutable = true; actionName = "$hmr"; try { Object.keys(raw).forEach(function (name) { delete raw[name]; }); Object.assign(raw, nextShape); installActions(); persistState(); } finally { mutable = false; actionName = "direct"; } const result = { store: currentDefinition.name, preserved, reset, added, removed }; listeners.forEach(function (listener) { listener(instance.snapshot(), { store: currentDefinition.name, action: "$hmr", changed: added.concat(reset, removed) }); }); try { __root.dispatchEvent && __root.dispatchEvent(new CustomEvent("wrnexus:store-hmr", { detail: result })); } catch (_) {} return result; }, }; const instance = new Proxy(core, { get(target, property, receiver) { if (Reflect.has(target, property)) return Reflect.get(target, property, receiver); if (property in actions) return actions[property]; if (property in (currentDefinition.computed || {})) return currentDefinition.computed[property](state); return state[property]; }, set() { throw new TypeError("WRN-STORE-READONLY: store state is readonly outside actions"); }, }); __registry.set(key, instance); const hydrationSource = __hydration[currentDefinition.name]; const init = async function () { const run = async function (name, hook) { if (!hook) return; mutable = true; actionName = name; try { await hook({ state, runtime: "client", routeId, server }); } finally { mutable = false; actionName = "direct"; } }; await run("$clientInit", currentDefinition.lifecycle && currentDefinition.lifecycle.clientInit); if (hydrationSource) await run("$hydrate", currentDefinition.lifecycle && currentDefinition.lifecycle.hydrate); }; core.whenReady = init(); return instance; } if (!__root.__wrnexusApplyStoreHotUpdate) { __root.__wrnexusApplyStoreHotUpdate = async function (name, definition) { const results = []; for (const item of Array.from(__registry.values())) if (item.name === name && typeof item.__hotUpdate === "function") results.push(await item.__hotUpdate(definition)); return results; }; } if (!__root.__wrnexusStoreContainer) { __root.__wrnexusStoreContainer = { async disposePageStores() { for (const item of Array.from(__registry.values())) if (item.kind === "page") await item.dispose(); }, async hotUpdate(name, definition) { return __root.__wrnexusApplyStoreHotUpdate(name, definition); }, inspect() { return Array.from(__registry.values()).map(function (item) { return { name: item.name, kind: item.kind, state: item.snapshot() }; }); }, }; } export const ${ast.name}Definition = { name: ${JSON.stringify(ast.name)}, kind: ${JSON.stringify(ast.storeKind)}, createState: () => (${initialState}), computed: { ${computed} }, actions: { ${actions} }, persist: ${persistence}, lifecycle: { ${lifecycleEntries} }, }; export const ${ast.name} = __create(${ast.name}Definition); export default ${ast.name}; `; }