AppView in a box as a Vite plugin thing hatk.dev
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add customFetch parameter to generated callXrpc for SSR deduplication

When a custom fetch function is provided (e.g. SvelteKit's fetch),
bypass the server bridge and use relative URLs so the framework can
deduplicate server/client requests. Guards window.location redirects
for safe server-side execution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+16 -10
+16 -10
packages/hatk/src/cli.ts
··· 1813 1813 clientOut += `export async function callXrpc<K extends keyof XrpcSchema & string>(\n` 1814 1814 clientOut += ` nsid: K,\n` 1815 1815 clientOut += ` arg?: CallArg<K>,\n` 1816 + clientOut += ` customFetch?: typeof globalThis.fetch,\n` 1816 1817 clientOut += `): Promise<OutputOf<K>> {\n` 1817 - // Server-side bridge 1818 - clientOut += ` if (typeof window === 'undefined') {\n` 1818 + // Server-side bridge (skip when customFetch is provided — let SvelteKit's fetch handle it) 1819 + clientOut += ` if (typeof window === 'undefined' && !customFetch) {\n` 1819 1820 clientOut += ` const bridge = (globalThis as any).__hatk_callXrpc\n` 1820 1821 clientOut += ` if (!bridge) throw new Error('callXrpc: server bridge not available — is hatk initialized?')\n` 1821 1822 if (procedureNsids.length > 0 || blobInputNsids.length > 0) { ··· 1826 1827 } 1827 1828 clientOut += ` return bridge(nsid, arg) as Promise<OutputOf<K>>\n` 1828 1829 clientOut += ` }\n` 1829 - // Client-side fetch 1830 - clientOut += ` const url = new URL(\`/xrpc/\${nsid}\`, window.location.origin)\n` 1830 + // Client-side fetch (or server-side with customFetch for SSR deduplication) 1831 + clientOut += ` const _fetch = customFetch ?? globalThis.fetch\n` 1832 + clientOut += ` // Use relative URL so SvelteKit's fetch can deduplicate server/client requests\n` 1833 + clientOut += ` let path = \`/xrpc/\${nsid}\`\n` 1831 1834 if (blobInputNsids.length > 0) { 1832 1835 clientOut += ` if (_blobInputs.has(nsid)) {\n` 1833 1836 clientOut += ` const blob = arg as Blob | ArrayBuffer\n` 1834 1837 clientOut += ` const ct = blob instanceof Blob ? blob.type : 'application/octet-stream'\n` 1835 - clientOut += ` const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': ct }, body: blob })\n` 1838 + clientOut += ` const res = await _fetch(path, { method: 'POST', headers: { 'Content-Type': ct }, body: blob })\n` 1836 1839 clientOut += ` if (!res.ok) throw new Error(\`XRPC \${nsid} failed: \${res.status}\`)\n` 1837 1840 clientOut += ` return res.json() as Promise<OutputOf<K>>\n` 1838 1841 clientOut += ` }\n` 1839 1842 } 1840 1843 if (procedureNsids.length > 0) { 1841 1844 clientOut += ` if (_procedures.has(nsid)) {\n` 1842 - clientOut += ` const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(arg) })\n` 1843 - clientOut += ` if (res.status === 401) { window.location.href = '/oauth/login'; return new Promise(() => {}) as any }\n` 1845 + clientOut += ` const res = await _fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(arg) })\n` 1846 + clientOut += ` if (typeof window !== 'undefined' && res.status === 401) { window.location.href = '/oauth/login'; return new Promise(() => {}) as any }\n` 1844 1847 clientOut += ` if (!res.ok) throw new Error(\`XRPC \${nsid} failed: \${res.status}\`)\n` 1845 1848 clientOut += ` return res.json() as Promise<OutputOf<K>>\n` 1846 1849 clientOut += ` }\n` 1847 1850 } 1851 + clientOut += ` const params = new URLSearchParams()\n` 1848 1852 clientOut += ` for (const [k, v] of Object.entries(arg || {})) {\n` 1849 - clientOut += ` if (v != null) url.searchParams.set(k, String(v))\n` 1853 + clientOut += ` if (v != null) params.set(k, String(v))\n` 1850 1854 clientOut += ` }\n` 1851 - clientOut += ` const res = await fetch(url)\n` 1852 - clientOut += ` if (res.status === 401) { window.location.href = '/oauth/login'; return new Promise(() => {}) as any }\n` 1855 + clientOut += ` const qs = params.toString()\n` 1856 + clientOut += ` if (qs) path += \`?\${qs}\`\n` 1857 + clientOut += ` const res = await _fetch(path)\n` 1858 + clientOut += ` if (typeof window !== 'undefined' && res.status === 401) { window.location.href = '/oauth/login'; return new Promise(() => {}) as any }\n` 1853 1859 clientOut += ` if (!res.ok) throw new Error(\`XRPC \${nsid} failed: \${res.status}\`)\n` 1854 1860 clientOut += ` return res.json() as Promise<OutputOf<K>>\n` 1855 1861 clientOut += `}\n`