@invisibleloop/pulse 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/.claude/commands/build-page.md +59 -0
  2. package/.claude/commands/new-doc-page.md +45 -0
  3. package/.claude/commands/verify.md +52 -0
  4. package/.claude/pulse-checklist.md +111 -0
  5. package/.claude/settings.local.json +102 -0
  6. package/.github/workflows/ci.yml +22 -0
  7. package/.github/workflows/publish.yml +41 -0
  8. package/.pulse/load-reports/home/1773432711417.json +22 -0
  9. package/CLAUDE.md +383 -0
  10. package/README.md +95 -0
  11. package/docs/.claude/pulse-checklist.md +111 -0
  12. package/docs/public/.pulse-ui-version +1 -0
  13. package/docs/public/dist/accessibility.boot-5DVTARJU.js +115 -0
  14. package/docs/public/dist/actions.boot-P66HKQEM.js +164 -0
  15. package/docs/public/dist/auth.boot-IMAJAUPH.js +140 -0
  16. package/docs/public/dist/caching.boot-DVR6KDE7.js +53 -0
  17. package/docs/public/dist/components--accordion.boot-3HVKMNWC.js +11 -0
  18. package/docs/public/dist/components--alert.boot-GCEXOZAC.js +6 -0
  19. package/docs/public/dist/components--app-badge.boot-DVT3GCHJ.js +6 -0
  20. package/docs/public/dist/components--avatar.boot-PSW24EVA.js +5 -0
  21. package/docs/public/dist/components--badge.boot-TYDY2RMK.js +7 -0
  22. package/docs/public/dist/components--banner.boot-EI5PZSZK.js +7 -0
  23. package/docs/public/dist/components--breadcrumbs.boot-SMA2E2GO.js +34 -0
  24. package/docs/public/dist/components--button.boot-J54BQM2E.js +23 -0
  25. package/docs/public/dist/components--card.boot-PZGNDIB6.js +138 -0
  26. package/docs/public/dist/components--carousel.boot-TP6LPFZZ.js +12 -0
  27. package/docs/public/dist/components--charts.boot-2EOYQWKL.js +108 -0
  28. package/docs/public/dist/components--checkbox.boot-DS5BSL6T.js +54 -0
  29. package/docs/public/dist/components--cluster.boot-HHVIBBJG.js +9 -0
  30. package/docs/public/dist/components--code-window.boot-2GR2DV33.js +20 -0
  31. package/docs/public/dist/components--container.boot-7LOOGK2K.js +5 -0
  32. package/docs/public/dist/components--cta.boot-FSNZ5YRT.js +11 -0
  33. package/docs/public/dist/components--divider.boot-3NI2C3QG.js +6 -0
  34. package/docs/public/dist/components--empty.boot-YX2UR3PV.js +7 -0
  35. package/docs/public/dist/components--feature.boot-MUD7NSUO.js +13 -0
  36. package/docs/public/dist/components--fieldset.boot-J7BYHMKF.js +19 -0
  37. package/docs/public/dist/components--fileupload.boot-NIKVTTPD.js +52 -0
  38. package/docs/public/dist/components--footer.boot-EYUK5FRG.js +14 -0
  39. package/docs/public/dist/components--grid.boot-URDQVDDR.js +59 -0
  40. package/docs/public/dist/components--heading.boot-BPQKU43E.js +44 -0
  41. package/docs/public/dist/components--hero.boot-4RAPRGAB.js +17 -0
  42. package/docs/public/dist/components--icons.boot-ZITNU5JP.js +68 -0
  43. package/docs/public/dist/components--image.boot-XEEGHQZF.js +19 -0
  44. package/docs/public/dist/components--input.boot-SGASZG5K.js +7 -0
  45. package/docs/public/dist/components--list.boot-W3XC5MHD.js +55 -0
  46. package/docs/public/dist/components--media.boot-5VFIETZO.js +13 -0
  47. package/docs/public/dist/components--modal.boot-RZUYXBN2.js +47 -0
  48. package/docs/public/dist/components--nav.boot-ODBOHU7O.js +33 -0
  49. package/docs/public/dist/components--pricing.boot-4AQ4ZVBY.js +21 -0
  50. package/docs/public/dist/components--progress.boot-GHAGYZOK.js +30 -0
  51. package/docs/public/dist/components--prose.boot-QANJL6JI.js +67 -0
  52. package/docs/public/dist/components--pullquote.boot-Q2WMNAZU.js +22 -0
  53. package/docs/public/dist/components--radio.boot-TJRDQ2OL.js +75 -0
  54. package/docs/public/dist/components--rating.boot-QBAN6DEL.js +38 -0
  55. package/docs/public/dist/components--search.boot-PXH5O5AG.js +17 -0
  56. package/docs/public/dist/components--section.boot-AQGIYHWW.js +12 -0
  57. package/docs/public/dist/components--segmented.boot-BEVTKEJO.js +33 -0
  58. package/docs/public/dist/components--select.boot-47X5RHOC.js +10 -0
  59. package/docs/public/dist/components--slider.boot-PSRRX7XL.js +47 -0
  60. package/docs/public/dist/components--spinner.boot-MZ5MO2OH.js +22 -0
  61. package/docs/public/dist/components--stack.boot-DI4NJXBF.js +9 -0
  62. package/docs/public/dist/components--stat.boot-QMFUWBQT.js +9 -0
  63. package/docs/public/dist/components--stepper.boot-34PP2NEV.js +22 -0
  64. package/docs/public/dist/components--table.boot-FCQGSFIQ.js +11 -0
  65. package/docs/public/dist/components--testimonial.boot-DWQPDKYG.js +11 -0
  66. package/docs/public/dist/components--textarea.boot-QVXLBOJ5.js +4 -0
  67. package/docs/public/dist/components--timeline.boot-26LN52P2.js +95 -0
  68. package/docs/public/dist/components--toggle.boot-IQQEI76S.js +29 -0
  69. package/docs/public/dist/components--tooltip.boot-LGHCO6NN.js +9 -0
  70. package/docs/public/dist/components.boot-SE6PQ4P7.js +103 -0
  71. package/docs/public/dist/config.boot-DTRRWUE6.js +126 -0
  72. package/docs/public/dist/constraints.boot-DUHDZBMC.js +71 -0
  73. package/docs/public/dist/deploy.boot-SLAD3NI2.js +163 -0
  74. package/docs/public/dist/docs-8e3d4b5c.css +1 -0
  75. package/docs/public/dist/extending.boot-UA3CN243.js +159 -0
  76. package/docs/public/dist/faq.boot-6EQAWLQR.js +43 -0
  77. package/docs/public/dist/getting-started.boot-TDKIFL5U.js +86 -0
  78. package/docs/public/dist/guard.boot-AUHAWTG4.js +80 -0
  79. package/docs/public/dist/home.boot-BVQXRH32.js +383 -0
  80. package/docs/public/dist/how-it-works.boot-LTWAKWKW.js +104 -0
  81. package/docs/public/dist/hydration.boot-JRM6IPJL.js +78 -0
  82. package/docs/public/dist/images.boot-M6ZVKTZS.js +80 -0
  83. package/docs/public/dist/manifest.json +94 -0
  84. package/docs/public/dist/meta.boot-7NXGPHR4.js +79 -0
  85. package/docs/public/dist/mutations.boot-F6F43UDX.js +79 -0
  86. package/docs/public/dist/navigation.boot-AOXWS3ZF.js +57 -0
  87. package/docs/public/dist/performance.boot-C3UPCOBK.js +98 -0
  88. package/docs/public/dist/persist.boot-WT32PQOQ.js +61 -0
  89. package/docs/public/dist/project-structure.boot-FB3LRVJ4.js +63 -0
  90. package/docs/public/dist/prompt-examples.boot-YKR4VDK4.js +31 -0
  91. package/docs/public/dist/pulse-ui-81a85c03.css +1 -0
  92. package/docs/public/dist/raw-responses.boot-M4KA5YXL.js +104 -0
  93. package/docs/public/dist/routing.boot-FNX5FDGH.js +70 -0
  94. package/docs/public/dist/runtime-B73WLANC.js +1 -0
  95. package/docs/public/dist/runtime-KO4BHUQ3.js +49 -0
  96. package/docs/public/dist/runtime-L2HNXIHW.js +59 -0
  97. package/docs/public/dist/runtime-QFURDKA2.js +5 -0
  98. package/docs/public/dist/runtime-UVPXO4IR.js +375 -0
  99. package/docs/public/dist/runtime-VMJA3Z4N.js +10 -0
  100. package/docs/public/dist/runtime-ZJ4FXT5O.js +11 -0
  101. package/docs/public/dist/server-api.boot-K7X3LCFB.js +219 -0
  102. package/docs/public/dist/server-data.boot-Y7HQYC4R.js +157 -0
  103. package/docs/public/dist/slash-commands.boot-V2UV7OW2.js +26 -0
  104. package/docs/public/dist/spec.boot-2WU7ZHCV.js +159 -0
  105. package/docs/public/dist/state.boot-B24GUE3R.js +73 -0
  106. package/docs/public/dist/store.boot-TLIB4XHH.js +150 -0
  107. package/docs/public/dist/streaming.boot-W2DZSMW4.js +80 -0
  108. package/docs/public/dist/stripe.boot-QN3C2GEL.js +164 -0
  109. package/docs/public/dist/supabase.boot-BG4XXLZE.js +303 -0
  110. package/docs/public/dist/testing.boot-6U4WKMTE.js +130 -0
  111. package/docs/public/dist/validation.boot-PQHYGW5B.js +100 -0
  112. package/docs/public/docs.css +2020 -0
  113. package/docs/public/menu.js +83 -0
  114. package/docs/public/pulse-ui.css +2739 -0
  115. package/docs/public/pulse-ui.js +236 -0
  116. package/docs/server.js +192 -0
  117. package/docs/src/lib/component-page.js +47 -0
  118. package/docs/src/lib/highlight.js +255 -0
  119. package/docs/src/lib/layout.js +131 -0
  120. package/docs/src/lib/metrics-store.js +6 -0
  121. package/docs/src/lib/nav.js +159 -0
  122. package/docs/src/lib/stats.js +81 -0
  123. package/docs/src/pages/accessibility.js +157 -0
  124. package/docs/src/pages/actions.js +191 -0
  125. package/docs/src/pages/auth.js +177 -0
  126. package/docs/src/pages/caching.js +95 -0
  127. package/docs/src/pages/components/accordion.js +48 -0
  128. package/docs/src/pages/components/alert.js +35 -0
  129. package/docs/src/pages/components/app-badge.js +41 -0
  130. package/docs/src/pages/components/avatar.js +35 -0
  131. package/docs/src/pages/components/badge.js +36 -0
  132. package/docs/src/pages/components/banner.js +45 -0
  133. package/docs/src/pages/components/breadcrumbs.js +94 -0
  134. package/docs/src/pages/components/button.js +84 -0
  135. package/docs/src/pages/components/card.js +225 -0
  136. package/docs/src/pages/components/carousel.js +72 -0
  137. package/docs/src/pages/components/charts.js +278 -0
  138. package/docs/src/pages/components/checkbox.js +129 -0
  139. package/docs/src/pages/components/cluster.js +47 -0
  140. package/docs/src/pages/components/code-window.js +57 -0
  141. package/docs/src/pages/components/container.js +40 -0
  142. package/docs/src/pages/components/cta.js +53 -0
  143. package/docs/src/pages/components/divider.js +37 -0
  144. package/docs/src/pages/components/empty.js +36 -0
  145. package/docs/src/pages/components/feature.js +60 -0
  146. package/docs/src/pages/components/fieldset.js +65 -0
  147. package/docs/src/pages/components/fileupload.js +127 -0
  148. package/docs/src/pages/components/footer.js +58 -0
  149. package/docs/src/pages/components/grid.js +165 -0
  150. package/docs/src/pages/components/heading.js +107 -0
  151. package/docs/src/pages/components/hero.js +65 -0
  152. package/docs/src/pages/components/icons.js +285 -0
  153. package/docs/src/pages/components/image.js +71 -0
  154. package/docs/src/pages/components/input.js +51 -0
  155. package/docs/src/pages/components/list.js +112 -0
  156. package/docs/src/pages/components/media.js +51 -0
  157. package/docs/src/pages/components/modal.js +111 -0
  158. package/docs/src/pages/components/nav.js +86 -0
  159. package/docs/src/pages/components/pricing.js +68 -0
  160. package/docs/src/pages/components/progress.js +102 -0
  161. package/docs/src/pages/components/prose.js +111 -0
  162. package/docs/src/pages/components/pullquote.js +71 -0
  163. package/docs/src/pages/components/radio.js +194 -0
  164. package/docs/src/pages/components/rating.js +106 -0
  165. package/docs/src/pages/components/search.js +61 -0
  166. package/docs/src/pages/components/section.js +59 -0
  167. package/docs/src/pages/components/segmented.js +121 -0
  168. package/docs/src/pages/components/select.js +45 -0
  169. package/docs/src/pages/components/slider.js +114 -0
  170. package/docs/src/pages/components/spinner.js +73 -0
  171. package/docs/src/pages/components/stack.js +48 -0
  172. package/docs/src/pages/components/stat.js +55 -0
  173. package/docs/src/pages/components/stepper.js +66 -0
  174. package/docs/src/pages/components/table.js +45 -0
  175. package/docs/src/pages/components/testimonial.js +49 -0
  176. package/docs/src/pages/components/textarea.js +31 -0
  177. package/docs/src/pages/components/timeline.js +227 -0
  178. package/docs/src/pages/components/toggle.js +84 -0
  179. package/docs/src/pages/components/tooltip.js +48 -0
  180. package/docs/src/pages/components.js +204 -0
  181. package/docs/src/pages/config.js +193 -0
  182. package/docs/src/pages/constraints.js +99 -0
  183. package/docs/src/pages/deploy.js +233 -0
  184. package/docs/src/pages/extending.js +198 -0
  185. package/docs/src/pages/faq.js +96 -0
  186. package/docs/src/pages/getting-started.js +106 -0
  187. package/docs/src/pages/guard.js +121 -0
  188. package/docs/src/pages/home.js +401 -0
  189. package/docs/src/pages/how-it-works.js +183 -0
  190. package/docs/src/pages/hydration.js +98 -0
  191. package/docs/src/pages/images.js +121 -0
  192. package/docs/src/pages/meta.js +120 -0
  193. package/docs/src/pages/mutations.js +106 -0
  194. package/docs/src/pages/navigation.js +85 -0
  195. package/docs/src/pages/performance.js +157 -0
  196. package/docs/src/pages/persist.js +88 -0
  197. package/docs/src/pages/project-structure.js +90 -0
  198. package/docs/src/pages/prompt-examples.js +186 -0
  199. package/docs/src/pages/raw-responses.js +124 -0
  200. package/docs/src/pages/routing.js +99 -0
  201. package/docs/src/pages/server-api.js +281 -0
  202. package/docs/src/pages/server-data.js +185 -0
  203. package/docs/src/pages/slash-commands.js +55 -0
  204. package/docs/src/pages/spec.js +207 -0
  205. package/docs/src/pages/state.js +101 -0
  206. package/docs/src/pages/store.js +181 -0
  207. package/docs/src/pages/streaming.js +108 -0
  208. package/docs/src/pages/stripe.js +193 -0
  209. package/docs/src/pages/supabase.js +323 -0
  210. package/docs/src/pages/testing.js +198 -0
  211. package/docs/src/pages/validation.js +138 -0
  212. package/examples/contact.js +166 -0
  213. package/examples/counter.js +94 -0
  214. package/examples/dev.server.js +91 -0
  215. package/examples/examples.test.js +394 -0
  216. package/examples/pricing.js +244 -0
  217. package/examples/products.js +191 -0
  218. package/examples/quiz.js +208 -0
  219. package/examples/shared.js +78 -0
  220. package/examples/todos.js +162 -0
  221. package/package.json +75 -0
  222. package/public/.pulse-ui-version +1 -0
  223. package/public/chippy-bird.css +246 -0
  224. package/public/examples/contact.css +119 -0
  225. package/public/examples/counter.css +79 -0
  226. package/public/examples/pricing.css +132 -0
  227. package/public/examples/products.css +100 -0
  228. package/public/examples/quiz.css +200 -0
  229. package/public/examples/todos.css +137 -0
  230. package/public/favicon.ico +0 -0
  231. package/public/log-dashboard.css +383 -0
  232. package/public/pulse-ui.css +2740 -0
  233. package/public/pulse-ui.js +236 -0
  234. package/public/pulse.css +149 -0
  235. package/scripts/build.js +411 -0
  236. package/src/agent/checklist.md +111 -0
  237. package/src/agent/coverage-check.js +66 -0
  238. package/src/agent/guide-components.md +274 -0
  239. package/src/agent/guide-examples.md +54 -0
  240. package/src/agent/guide-routing.md +36 -0
  241. package/src/agent/guide-server.md +258 -0
  242. package/src/agent/guide-spec.md +103 -0
  243. package/src/agent/guide-styles.md +191 -0
  244. package/src/agent/guide.md +979 -0
  245. package/src/agent/identity.md +106 -0
  246. package/src/agent/workflow.md +108 -0
  247. package/src/cli/cli.test.js +82 -0
  248. package/src/cli/dev.js +195 -0
  249. package/src/cli/discover.js +113 -0
  250. package/src/cli/index.js +361 -0
  251. package/src/cli/load-report.js +91 -0
  252. package/src/cli/load-runner.js +121 -0
  253. package/src/cli/report-server.js +723 -0
  254. package/src/cli/report.js +116 -0
  255. package/src/cli/scaffold.archive.js +1371 -0
  256. package/src/cli/scaffold.js +349 -0
  257. package/src/cli/start.js +74 -0
  258. package/src/html.js +19 -0
  259. package/src/mcp/server.js +884 -0
  260. package/src/mcp/validate-worker.js +110 -0
  261. package/src/runtime/image.js +74 -0
  262. package/src/runtime/image.test.js +111 -0
  263. package/src/runtime/index.js +621 -0
  264. package/src/runtime/navigate.js +146 -0
  265. package/src/runtime/runtime.test.js +773 -0
  266. package/src/runtime/ssr.js +464 -0
  267. package/src/runtime/ssr.test.js +421 -0
  268. package/src/runtime/store.js +92 -0
  269. package/src/runtime/toast.js +163 -0
  270. package/src/server/index.js +1386 -0
  271. package/src/server/server.test.js +1248 -0
  272. package/src/spec/schema.js +428 -0
  273. package/src/spec/schema.test.js +291 -0
  274. package/src/store/index.js +102 -0
  275. package/src/store/store.test.js +210 -0
  276. package/src/testing/html.js +283 -0
  277. package/src/testing/index.js +249 -0
  278. package/src/testing/testing.test.js +450 -0
  279. package/src/ui/accordion.js +28 -0
  280. package/src/ui/alert.js +43 -0
  281. package/src/ui/app-badge.js +48 -0
  282. package/src/ui/avatar.js +47 -0
  283. package/src/ui/badge.js +24 -0
  284. package/src/ui/banner.js +26 -0
  285. package/src/ui/breadcrumbs.js +38 -0
  286. package/src/ui/button.js +66 -0
  287. package/src/ui/card.js +34 -0
  288. package/src/ui/carousel.js +59 -0
  289. package/src/ui/charts.js +321 -0
  290. package/src/ui/checkbox.js +65 -0
  291. package/src/ui/cluster.js +44 -0
  292. package/src/ui/code-window.js +39 -0
  293. package/src/ui/container.js +24 -0
  294. package/src/ui/cta.js +37 -0
  295. package/src/ui/divider.js +29 -0
  296. package/src/ui/empty.js +33 -0
  297. package/src/ui/feature.js +33 -0
  298. package/src/ui/fieldset.js +37 -0
  299. package/src/ui/fileupload.js +89 -0
  300. package/src/ui/footer.js +38 -0
  301. package/src/ui/grid.js +36 -0
  302. package/src/ui/heading.js +45 -0
  303. package/src/ui/hero.js +37 -0
  304. package/src/ui/icons.js +161 -0
  305. package/src/ui/index.js +89 -0
  306. package/src/ui/input.js +74 -0
  307. package/src/ui/list.js +36 -0
  308. package/src/ui/media.js +44 -0
  309. package/src/ui/modal.js +80 -0
  310. package/src/ui/nav.js +61 -0
  311. package/src/ui/pricing.js +56 -0
  312. package/src/ui/progress.js +62 -0
  313. package/src/ui/prose.js +29 -0
  314. package/src/ui/pullquote.js +34 -0
  315. package/src/ui/radio.js +102 -0
  316. package/src/ui/rating.js +93 -0
  317. package/src/ui/search.js +77 -0
  318. package/src/ui/section.js +69 -0
  319. package/src/ui/segmented.js +50 -0
  320. package/src/ui/select.js +77 -0
  321. package/src/ui/slider.js +84 -0
  322. package/src/ui/spinner.js +34 -0
  323. package/src/ui/stack.js +36 -0
  324. package/src/ui/stat.js +52 -0
  325. package/src/ui/stepper.js +46 -0
  326. package/src/ui/switch.js +57 -0
  327. package/src/ui/table.js +45 -0
  328. package/src/ui/testimonial.js +48 -0
  329. package/src/ui/textarea.js +72 -0
  330. package/src/ui/timeline.js +72 -0
  331. package/src/ui/tooltip.js +28 -0
  332. package/src/ui/ui.test.js +1241 -0
  333. package/src/ui/uiimage.js +65 -0
  334. package/tsconfig.json +13 -0
  335. package/types/html.d.ts +17 -0
  336. package/types/image.d.ts +70 -0
  337. package/types/index.d.ts +7 -0
  338. package/types/navigate.d.ts +38 -0
  339. package/types/runtime.d.ts +63 -0
  340. package/types/schema.d.ts +243 -0
  341. package/types/server.d.ts +145 -0
  342. package/types/ssr.d.ts +110 -0
  343. package/types/testing.d.ts +154 -0
  344. package/types/ui.d.ts +704 -0
@@ -0,0 +1,207 @@
1
+ import { renderLayout, h1, lead, section, codeBlock, table, callout } from '../lib/layout.js'
2
+ import { prevNext } from '../lib/nav.js'
3
+ import { highlight } from '../lib/highlight.js'
4
+
5
+ const { prev, next } = prevNext('/spec')
6
+
7
+ export default {
8
+ route: '/spec',
9
+ meta: {
10
+ title: 'Spec Reference — Pulse Docs',
11
+ description: 'Complete reference for every field in a Pulse page spec.',
12
+ styles: ['/docs.css'],
13
+ },
14
+ state: {},
15
+ view: () => renderLayout({
16
+ currentHref: '/spec',
17
+ prev,
18
+ next,
19
+ content: `
20
+ ${h1('Spec Reference')}
21
+ ${lead('The spec is a plain JavaScript object that defines a complete contract for a page. Pulse validates every spec at startup and rejects invalid ones before the server accepts connections. At runtime, it enforces state bounds, validation rules, and lifecycle order automatically.')}
22
+
23
+ ${section('quick-ref', 'Quick reference')}
24
+ ${table(
25
+ ['Field', 'Type', 'Required', 'Description'],
26
+ [
27
+ ['<code>route</code>', '<code>string</code>', 'Yes', 'URL pattern for this page. Supports <code>:param</code> segments.'],
28
+ ['<code>state</code>', '<code>object</code>', 'Yes', 'Initial client-side state. Deep-cloned on mount.'],
29
+ ['<code>view</code>', '<code>function</code>', 'Yes', 'Returns an HTML string. Receives <code>(state, serverState)</code>.'],
30
+ ['<code>meta</code>', '<code>object</code>', 'No', 'Page metadata: title, description, styles, OG tags, schema.'],
31
+ ['<code>hydrate</code>', '<code>string</code>', 'No', 'Browser-importable path to this spec file. Enables client hydration.'],
32
+ ['<code>mutations</code>', '<code>object</code>', 'No', 'Synchronous state updaters keyed by name.'],
33
+ ['<code>actions</code>', '<code>object</code>', 'No', 'Async operations with full lifecycle hooks.'],
34
+ ['<code>validation</code>', '<code>object</code>', 'No', 'Declarative validation rules keyed by dot-path state keys.'],
35
+ ['<code>constraints</code>', '<code>object</code>', 'No', 'Min/max bounds enforced after every mutation.'],
36
+ ['<code>persist</code>', '<code>string[]</code>', 'No', 'State keys to save in <code>localStorage</code>.'],
37
+ ['<code>server</code>', '<code>object</code>', 'No', 'Server-side data fetcher. Result passed to <code>view</code> as second arg.'],
38
+ ['<code>store</code>', '<code>string[]</code>', 'No', 'Global store keys this page subscribes to. See <a href="/store">Global Store</a>.'],
39
+ ['<code>methods</code>', '<code>string[]</code>', 'No', 'HTTP methods this page accepts. Default <code>[\'GET\', \'HEAD\']</code>. Add <code>\'POST\'</code> etc. to opt in.'],
40
+ ['<code>stream</code>', '<code>object</code>', 'No', 'Streaming SSR config: <code>shell</code> + <code>deferred</code> segment names.'],
41
+ ['<code>cache</code>', '<code>object</code>', 'No', 'HTTP cache control headers for the page response.'],
42
+ ['<code>serverTtl</code>', '<code>number</code>', 'No', 'Seconds to cache server data in-process.'],
43
+ ['<code>serverTimeout</code>', '<code>number</code>', 'No', 'Timeout in ms for all server fetchers on this page. Overrides the global <code>fetcherTimeout</code> option.'],
44
+ ['<code>contentType</code>', '<code>string</code>', 'No', 'Override response Content-Type. Enables raw (non-HTML) responses.'],
45
+ ['<code>onViewError</code>', '<code>function</code>', 'No', 'Fallback renderer called when <code>view()</code> throws. Return an HTML string.'],
46
+ ]
47
+ )}
48
+
49
+ ${section('route', 'route')}
50
+ <p>The URL pattern this spec handles. Supports static segments and dynamic <code>:param</code> segments.</p>
51
+ ${codeBlock(highlight(`route: '/products/:id' // matches /products/42
52
+ route: '/blog/:year/:slug'`, 'js'))}
53
+ <p>Dynamic segments are available in server data and actions via <code>ctx.params</code>. See <a href="/routing">Routing</a> for more.</p>
54
+
55
+ ${section('state', 'state')}
56
+ <p>The initial client-side state for the page. Always a plain object. Pulse deep-clones it on every mount — mutations never affect the original spec, and state cannot leak between page loads.</p>
57
+ ${codeBlock(highlight(`state: {
58
+ count: 0,
59
+ user: { name: '', email: '' },
60
+ items: [],
61
+ }`, 'js'))}
62
+ <p>The state object is passed as the first argument to <code>view</code>, and as the first argument to every mutation and action hook. See <a href="/state">State</a>.</p>
63
+
64
+ ${section('view', 'view')}
65
+ <p>A pure function that receives <code>(state, serverState)</code> and returns an HTML string. Side effects are not permitted — the same inputs must always produce the same output. Pulse uses this guarantee to diff and re-render efficiently after mutations.</p>
66
+ ${codeBlock(highlight(`view: (state, server) => \`
67
+ <main>
68
+ <h1>Hello, \${state.name}</h1>
69
+ \${server.items.map(item => \`<p>\${item.title}</p>\`).join('')}
70
+ </main>
71
+ \``, 'js'))}
72
+ <p>For streaming SSR, <code>view</code> can be an object of named segment functions. See <a href="/streaming">Streaming SSR</a>.</p>
73
+
74
+ ${section('meta', 'meta')}
75
+ <p>Page-level metadata. All fields are optional.</p>
76
+ ${codeBlock(highlight(`meta: {
77
+ title: 'Page Title — Site Name',
78
+ description: 'Meta description for search engines.',
79
+ styles: ['/app.css', '/page.css'],
80
+ ogTitle: 'Open Graph title',
81
+ ogImage: 'https://example.com/og.jpg',
82
+ schema: { '@type': 'WebPage', name: 'Page Title' }, // ld+json
83
+ }`, 'js'))}
84
+ <p>See <a href="/meta">Metadata &amp; SEO</a> for the full reference.</p>
85
+
86
+ ${section('hydrate', 'hydrate')}
87
+ <p>A browser-importable path to this spec file. Setting this enables client-side hydration — Pulse emits a bootstrap script that imports the spec bundle and calls <code>mount()</code>. In production, the path is resolved automatically via <code>manifest.json</code>.</p>
88
+ ${codeBlock(highlight(`hydrate: '/src/pages/counter.js' // dev: source file path
89
+ // Production: resolved automatically via manifest.json`, 'js'))}
90
+ ${callout('note', 'Omit <code>hydrate</code> for purely server-rendered pages with no client interactivity. Pulse sends zero JavaScript to the browser — no runtime overhead, no hydration cost.')}
91
+
92
+ ${section('mutations', 'mutations')}
93
+ <p>Synchronous state updaters. Each mutation is a function <code>(state, event) =&gt; partialState</code>. The returned partial object is merged into state. See <a href="/mutations">Mutations</a>.</p>
94
+ ${codeBlock(highlight(`mutations: {
95
+ increment: (state) => ({ count: state.count + 1 }),
96
+ setName: (state, event) => ({ name: event.target.value }),
97
+ }`, 'js'))}
98
+ <p>Mutations can return <code>_toast</code> to show a notification — it is stripped from state automatically. See <a href="/actions#toast">Toast notifications</a>.</p>
99
+
100
+ ${section('actions', 'actions')}
101
+ <p>Async operations with a full lifecycle. Each action has hooks for <code>onStart</code>, optional <code>validate</code>, <code>run</code>, <code>onSuccess</code>, and <code>onError</code>. See <a href="/actions">Actions</a>.</p>
102
+ ${codeBlock(highlight(`actions: {
103
+ submit: {
104
+ onStart: (state, formData) => ({ status: 'loading' }),
105
+ validate: true,
106
+ run: async (state, serverState, formData) => {
107
+ const res = await fetch('/api/submit', { method: 'POST', body: formData })
108
+ return res.json()
109
+ },
110
+ onSuccess: (state, payload) => ({ status: 'success', data: payload }),
111
+ onError: (state, err) => ({
112
+ status: 'error',
113
+ errors: err?.validation ?? [{ message: err.message }],
114
+ }),
115
+ },
116
+ }`, 'js'))}
117
+
118
+ ${section('validation', 'validation')}
119
+ <p>Declarative rules checked when an action has <code>validate: true</code>. Keys are dot-path strings into state. See <a href="/validation">Validation</a>.</p>
120
+ ${codeBlock(highlight(`validation: {
121
+ 'fields.email': { required: true, format: 'email' },
122
+ 'fields.name': { required: true, minLength: 2, maxLength: 100 },
123
+ 'fields.age': { required: true, min: 18, max: 120 },
124
+ }`, 'js'))}
125
+
126
+ ${section('constraints', 'constraints')}
127
+ <p>Min/max bounds enforced automatically after every mutation. Constraints cannot be bypassed — the state is clamped before the view re-renders, regardless of what the mutation returns. See <a href="/constraints">Constraints</a>.</p>
128
+ ${codeBlock(highlight(`constraints: {
129
+ count: { min: 0, max: 100 },
130
+ quantity: { min: 1, max: 99 },
131
+ }`, 'js'))}
132
+
133
+ ${section('persist', 'persist')}
134
+ <p>An array of dot-path state keys to persist in <code>localStorage</code>. Values are restored on the next visit. See <a href="/persist">Persist</a>.</p>
135
+ ${codeBlock(highlight(`persist: ['theme', 'user.preferences']`, 'js'))}
136
+
137
+ ${section('server', 'server')}
138
+ <p>Server-only data fetching. The result is passed to <code>view</code> as the second argument. Not available on the client. See <a href="/server-data">Server Data</a>.</p>
139
+ ${codeBlock(highlight(`server: {
140
+ data: async (ctx) => {
141
+ const product = await db.products.find(ctx.params.id)
142
+ return { product }
143
+ }
144
+ }`, 'js'))}
145
+
146
+ ${section('stream', 'stream')}
147
+ <p>Enables streaming SSR. Declare which named view segments are in the <code>shell</code> (rendered immediately) and which are <code>deferred</code> (streamed when ready). See <a href="/streaming">Streaming SSR</a>.</p>
148
+ ${codeBlock(highlight(`stream: {
149
+ shell: ['header', 'nav'],
150
+ deferred: ['feed', 'sidebar'],
151
+ }`, 'js'))}
152
+
153
+ ${section('cache', 'cache')}
154
+ <p>HTTP Cache-Control header configuration for the page response. See <a href="/caching">Caching</a>.</p>
155
+ ${codeBlock(highlight(`cache: {
156
+ public: true,
157
+ maxAge: 300, // seconds
158
+ staleWhileRevalidate: 86400,
159
+ }`, 'js'))}
160
+
161
+ ${section('serverTtl', 'serverTtl')}
162
+ <p>Number of seconds to cache the result of <code>server.data()</code> in-process. Subsequent requests within the TTL skip the async data fetch and re-render the HTML with the cached data. See <a href="/caching">Caching</a>.</p>
163
+ ${codeBlock(highlight(`serverTtl: 60 // cache server data for 60 seconds`, 'js'))}
164
+ ${callout('tip', '<strong>serverTtl vs cache</strong> — <code>serverTtl</code> caches only the server data fetcher results. The HTML is re-rendered on every request (good for personalised pages where only the fetched data is stable). <code>cache</code> caches the complete rendered HTML and sets <code>Cache-Control</code> headers (good for fully public pages that are identical for all users).')}
165
+
166
+ ${section('serverTimeout', 'serverTimeout')}
167
+ <p>Timeout in milliseconds for all <code>server.*</code> fetchers on this page. If any fetcher does not resolve within this limit, it rejects with a timeout error and the request returns a 500. Use this to prevent a slow DB query or external API from hanging the response indefinitely.</p>
168
+ ${codeBlock(highlight(`serverTimeout: 5000 // fail after 5 s — overrides createServer fetcherTimeout`, 'js'))}
169
+ <p>A global default applies to all pages via the <code>fetcherTimeout</code> option in <code>createServer</code>. <code>spec.serverTimeout</code> overrides it per page.</p>
170
+
171
+ ${section('on-view-error', 'onViewError')}
172
+ <p>An optional function called when <code>view()</code> throws at runtime. Return an HTML string to display in place of the crashed view. Without this, the Pulse runtime renders a default inline error message and logs the error to the console.</p>
173
+ ${codeBlock(highlight(`onViewError: (err, state, serverState) => \`
174
+ <div class="u-p-4 u-text-center">
175
+ <p>Something went wrong. <a href="">Reload</a></p>
176
+ </div>
177
+ \``, 'js'))}
178
+ ${callout('note', 'On the server, a throwing view propagates to the server\'s error handler (500 response) unless <code>onViewError</code> is defined — in which case the page renders with the fallback HTML and a 200 status. On the client, the runtime always catches view errors and shows a fallback, whether or not <code>onViewError</code> is defined.')}
179
+
180
+ ${section('methods', 'methods')}
181
+ <p>HTTP methods this page accepts. Defaults to <code>['GET', 'HEAD']</code> — all other methods return 405. Add <code>'POST'</code> to handle form submissions or webhooks directly on a page route without a separate API endpoint.</p>
182
+ ${codeBlock(highlight(`methods: ['GET', 'POST']`, 'js'))}
183
+ <p>Read the method in <code>guard</code> to branch on POST vs GET:</p>
184
+ ${codeBlock(highlight(`export default {
185
+ route: '/contact',
186
+ methods: ['GET', 'POST'],
187
+
188
+ guard: async (ctx) => {
189
+ if (ctx.method === 'POST') {
190
+ const data = await ctx.formData()
191
+ if (!data?.email) return { status: 422, json: { error: 'Email required' } }
192
+ await db.leads.create(data)
193
+ return { redirect: '/contact?sent=1' }
194
+ }
195
+ },
196
+
197
+ state: {},
198
+ view: (state) => \`<form method="POST">...</form>\`,
199
+ }`, 'js'))}
200
+ ${callout('note', 'For raw response specs (<code>contentType</code> set), all HTTP methods are accepted by default — <code>methods</code> has no effect. Use <code>ctx.method</code> inside <code>render</code> to branch.')}
201
+
202
+ ${section('contentType', 'contentType')}
203
+ <p>Override the response <code>Content-Type</code>. When set, the view function receives <code>(ctx, serverState)</code> and returns the raw response body — the normal HTML wrapper is bypassed. See <a href="/raw-responses">Raw Responses</a>.</p>
204
+ ${codeBlock(highlight(`contentType: 'application/rss+xml'`, 'js'))}
205
+ `,
206
+ }),
207
+ }
@@ -0,0 +1,101 @@
1
+ import { renderLayout, h1, lead, section, sub, codeBlock, callout, table } from '../lib/layout.js'
2
+ import { prevNext } from '../lib/nav.js'
3
+ import { highlight } from '../lib/highlight.js'
4
+
5
+ const { prev, next } = prevNext('/state')
6
+
7
+ export default {
8
+ route: '/state',
9
+ meta: {
10
+ title: 'State — Pulse Docs',
11
+ description: 'How client state is declared, initialised, and used in Pulse.',
12
+ styles: ['/docs.css'],
13
+ },
14
+ state: {},
15
+ view: () => renderLayout({
16
+ currentHref: '/state',
17
+ prev,
18
+ next,
19
+ content: `
20
+ ${h1('State')}
21
+ ${lead('Pulse enforces a strict one-way data flow. State is declared once in the spec, deep-cloned on mount, and changed only through mutations. Direct mutation is not possible — the framework prevents it by design.')}
22
+
23
+ ${section('declaring', 'Declaring state')}
24
+ <p>The <code>state</code> field of a spec is the initial value. Always a plain object — nested objects and arrays are supported:</p>
25
+ ${codeBlock(highlight(`export default {
26
+ route: '/checkout',
27
+ state: {
28
+ step: 1,
29
+ customer: {
30
+ name: '',
31
+ email: '',
32
+ },
33
+ items: [],
34
+ promoCode: null,
35
+ },
36
+ // ...
37
+ }`, 'js'))}
38
+ ${callout('note', '<code>state: {}</code> is always included, even on pages with no interactivity. The spec schema requires it.')}
39
+
40
+ ${section('view-receives', 'The view receives state')}
41
+ <p>The <code>view</code> function is called with the current state as its first argument. On first render this is the initial state (or state restored from <code>localStorage</code> if <code>persist</code> is set). After mutations it is the updated state:</p>
42
+ ${codeBlock(highlight(`view: (state) => \`
43
+ <div>
44
+ <p>Step \${state.step} of 3</p>
45
+ <p>Hello, \${state.customer.name || 'guest'}</p>
46
+ <ul>
47
+ \${state.items.map(item => \`<li>\${item.name}</li>\`).join('')}
48
+ </ul>
49
+ </div>
50
+ \``, 'js'))}
51
+
52
+ ${section('immutability', 'Immutability')}
53
+ <p>State is never mutated directly. Mutations are pure functions that return a <em>partial</em> object to merge — the framework rejects any other pattern:</p>
54
+ ${codeBlock(highlight(`// CORRECT — return a partial update
55
+ mutations: {
56
+ nextStep: (state) => ({ step: state.step + 1 }),
57
+ }
58
+
59
+ // WRONG — never mutate state directly
60
+ mutations: {
61
+ nextStep: (state) => { state.step++ }, // ✗ do not do this
62
+ }`, 'js'))}
63
+ <p>The runtime performs a shallow merge of the returned partial into the current state. This means top-level keys are replaced, not deep-merged:</p>
64
+ ${codeBlock(highlight(`// state = { step: 1, customer: { name: 'Alice', email: 'a@b.com' } }
65
+
66
+ mutations: {
67
+ // Only updates step — customer is untouched
68
+ nextStep: (state) => ({ step: state.step + 1 }),
69
+
70
+ // Replaces the entire customer object — spread to preserve email
71
+ setName: (state, e) => ({
72
+ customer: { ...state.customer, name: e.target.value }
73
+ }),
74
+ }`, 'js'))}
75
+
76
+ ${section('deep-clone', 'Deep clone on mount')}
77
+ <p>When the page mounts, Pulse deep-clones <code>spec.state</code>. This guarantees:</p>
78
+ <ul>
79
+ <li>The live state and the spec's initial state are completely independent — mutations cannot corrupt the spec.</li>
80
+ <li>Navigating away and back resets state to the spec's initial values (unless <a href="/persist">persisted</a>).</li>
81
+ <li>Multiple instances of the same spec on the same page each get independent state.</li>
82
+ </ul>
83
+
84
+ ${section('server-state', 'State vs server state')}
85
+ <p>Pulse draws a hard boundary between <em>client state</em> (the <code>state</code> field) and <em>server state</em> (from <code>server.data()</code>). Client state lives in the browser and changes in response to mutations. Server state is resolved before render and passed to the view as its second argument — it is never exposed to the client after hydration:</p>
86
+ ${codeBlock(highlight(`view: (state, server) => \`
87
+ <div>
88
+ <h1>\${server.product.name}</h1> <!-- server state -->
89
+ <p>Qty: \${state.quantity}</p> <!-- client state -->
90
+ </div>
91
+ \``, 'js'))}
92
+ ${callout('note', 'Server state is read-only and not available on the client after hydration. Anything that needs client interactivity belongs in <code>state</code>.')}
93
+
94
+ ${section('ssr-state', 'State during SSR')}
95
+ <p>On the server, the view is rendered with the spec's initial state. After hydration, <code>mount()</code> is called with <code>{ ssr: true }</code>, which skips the first client-side re-render and preserves the SSR-painted HTML exactly as the server sent it. This is what enables fast LCP — the initial HTML arrives from the server and the JS binds events without touching the DOM.</p>
96
+
97
+ ${section('persist-link', 'Persisting state')}
98
+ <p>State keys listed in the <a href="/persist"><code>persist</code></a> field are saved to <code>localStorage</code> and restored before the view renders on the next visit.</p>
99
+ `,
100
+ }),
101
+ }
@@ -0,0 +1,181 @@
1
+ import { renderLayout, h1, lead, section, codeBlock, callout, table } from '../lib/layout.js'
2
+ import { prevNext } from '../lib/nav.js'
3
+ import { highlight } from '../lib/highlight.js'
4
+
5
+ const { prev, next } = prevNext('/store')
6
+
7
+ export default {
8
+ route: '/store',
9
+ meta: {
10
+ title: 'Global Store — Pulse Docs',
11
+ description: 'Share server-fetched data across pages with a global store in Pulse.',
12
+ styles: ['/docs.css'],
13
+ },
14
+ state: {},
15
+ view: () => renderLayout({
16
+ currentHref: '/store',
17
+ prev,
18
+ next,
19
+ content: `
20
+ ${h1('Global Store')}
21
+ ${lead('The global store is a single shared data layer. Declare server fetchers once in <code>pulse.store.js</code> — user profiles, settings, feature flags — and any page can access them by name. No prop drilling, no repeated fetches.')}
22
+
23
+ ${section('when-to-use', 'When to use the store')}
24
+ <p>The store is the right tool when the same server data is needed on multiple pages and it would be wasteful to redeclare the same fetcher in every spec:</p>
25
+ <ul>
26
+ <li>Current user / session — <code>store.user</code></li>
27
+ <li>App settings or feature flags — <code>store.settings</code></li>
28
+ <li>Navigation items that come from a CMS — <code>store.nav</code></li>
29
+ <li>Subscription or plan level — <code>store.plan</code></li>
30
+ </ul>
31
+ ${callout('note', 'The store has no client-side reactivity. Data is fetched on the server per request and is available to the view at mount time. For page-specific data, use <a href="/server-data"><code>spec.server</code></a> instead.')}
32
+
33
+ ${section('defining', 'Defining the store')}
34
+ <p>Create a <code>pulse.store.js</code> file at the root of your project. Export a plain object with an optional <code>state</code> (default values) and a <code>server</code> map of async fetchers:</p>
35
+ ${codeBlock(highlight(`// pulse.store.js
36
+ export default {
37
+ // Default / fallback values used on the server before fetchers resolve
38
+ state: {
39
+ user: null,
40
+ settings: { theme: 'dark', lang: 'en' },
41
+ nav: [],
42
+ },
43
+
44
+ // Server fetchers — run once per request, results override state defaults
45
+ server: {
46
+ user: async (ctx) => db.users.findByCookie(ctx.cookies.session),
47
+ settings: async (ctx) => db.settings.forUser(ctx.cookies.userId),
48
+ nav: async () => cms.getNavItems(),
49
+ },
50
+ }`, 'js'))}
51
+
52
+ ${section('registering', 'Registering the store')}
53
+ <p>Pass your store to <code>createServer</code> via the <code>store</code> option. The store is validated at startup — bad fetchers throw before the server accepts connections:</p>
54
+ ${codeBlock(highlight(`import { createServer } from '@invisibleloop/pulse'
55
+ import store from './pulse.store.js'
56
+ import { dashboardSpec } from './src/pages/dashboard.js'
57
+ import { settingsSpec } from './src/pages/settings.js'
58
+
59
+ createServer([dashboardSpec, settingsSpec], {
60
+ port: 3000,
61
+ staticDir: 'public',
62
+ store, // ← register the global store
63
+ })`, 'js'))}
64
+
65
+ ${section('using', 'Using store data in a page')}
66
+ <p>Declare which store keys a page needs using the <code>store</code> field. Those keys are merged into the <code>server</code> argument of the view — alongside any page-level server data:</p>
67
+ ${codeBlock(highlight(`// src/pages/dashboard.js
68
+ export default {
69
+ route: '/dashboard',
70
+ store: ['user', 'settings'], // declare which store keys this page uses
71
+
72
+ // Page-level server data still works alongside store data
73
+ server: {
74
+ stats: async (ctx) => db.stats.forUser(ctx.store.user?.id),
75
+ },
76
+
77
+ state: { filter: 'week' },
78
+
79
+ view: (state, server) => \`
80
+ <main>
81
+ <h1>Hello, \${server.user?.name ?? 'there'}</h1>
82
+ <p>Theme: \${server.settings.theme}</p>
83
+ <p>Stats: \${server.stats.total} requests this \${state.filter}</p>
84
+ </main>
85
+ \`,
86
+ }`, 'js'))}
87
+ <p>Only the keys listed in <code>spec.store</code> are available in the view — nothing leaks from the store to pages that do not declare a dependency on it. Page-level <code>server</code> keys always win if there is a name collision with the store.</p>
88
+
89
+ ${section('ctx-store', 'Accessing the store in server fetchers')}
90
+ <p>Store data is resolved before page server fetchers run. The full resolved store state is available as <code>ctx.store</code> in any page's <code>server</code> fetcher, <code>guard</code>, and <code>meta</code> functions:</p>
91
+ ${codeBlock(highlight(`export default {
92
+ route: '/account',
93
+ store: ['user'],
94
+
95
+ // Guard can use ctx.store to check auth before fetching page data
96
+ guard: async (ctx) => {
97
+ if (!ctx.store.user) return { redirect: '/login' }
98
+ },
99
+
100
+ // Server fetchers receive ctx.store with the resolved store state
101
+ server: {
102
+ orders: async (ctx) => db.orders.forUser(ctx.store.user.id),
103
+ },
104
+
105
+ view: (state, server) => \`
106
+ <h1>Orders for \${server.user.name}</h1>
107
+ \`,
108
+ }`, 'js'))}
109
+
110
+ ${section('store-field-reference', 'Store field reference')}
111
+ ${table(
112
+ ['Field', 'Type', 'Description'],
113
+ [
114
+ ['<code>state</code>', 'object', 'Default values. Used as fallbacks when a server fetcher returns <code>undefined</code> or the server key is absent.'],
115
+ ['<code>server</code>', 'object of functions', 'Async fetchers — <code>async (ctx) => value</code>. Receive the same <code>ctx</code> as page server fetchers. Results override <code>state</code> defaults.'],
116
+ ]
117
+ )}
118
+
119
+ ${section('spec-store-reference', 'spec.store field')}
120
+ ${table(
121
+ ['Field', 'Type', 'Description'],
122
+ [
123
+ ['<code>store</code>', 'string[]', 'Array of store key strings to make available in the view\'s <code>server</code> argument. e.g. <code>[\'user\', \'settings\']</code>'],
124
+ ]
125
+ )}
126
+ ${callout('tip', 'Pages that do not declare <code>store</code> receive no store data — the store never leaks to pages that do not ask for it.')}
127
+
128
+ ${section('reactivity', 'Reactive updates — no refresh needed')}
129
+ <p>When a page action changes store data, all other mounted pages that subscribe to the affected keys re-render immediately — no page refresh, no polling.</p>
130
+ <p>Return <code>_storeUpdate</code> from a page action's <code>onSuccess</code> to push a change into the global store:</p>
131
+ ${codeBlock(highlight(`// src/pages/settings.js
132
+ export default {
133
+ route: '/settings',
134
+ store: ['settings'],
135
+ state: { saved: false },
136
+
137
+ actions: {
138
+ saveTheme: {
139
+ run: async (state, server, payload) => {
140
+ const theme = payload.get('theme')
141
+ await fetch('/api/settings', { method: 'PATCH', body: payload })
142
+ return theme
143
+ },
144
+ onSuccess: (state, theme) => ({
145
+ saved: true,
146
+ _storeUpdate: { settings: { theme } }, // ← push to global store
147
+ }),
148
+ onError: (state, err) => ({ error: err.message }),
149
+ },
150
+ },
151
+
152
+ view: (state, server) => \`
153
+ <form data-action="saveTheme">
154
+ <select name="theme">
155
+ <option value="dark" \${server.settings.theme === 'dark' ? 'selected' : ''}>Dark</option>
156
+ <option value="light" \${server.settings.theme === 'light' ? 'selected' : ''}>Light</option>
157
+ </select>
158
+ <button type="submit">Save</button>
159
+ \${state.saved ? '<p>Saved!</p>' : ''}
160
+ </form>
161
+ \`,
162
+ }`, 'js'))}
163
+ <p>Any other page that has <code>store: ['settings']</code> will re-render with the new theme value the moment <code>_storeUpdate</code> is dispatched — without navigating away or refreshing.</p>
164
+ ${callout('note', '<code>_storeUpdate</code> is stripped from the local page state — it is only forwarded to the store. The rest of the <code>onSuccess</code> return is merged into the page\'s own state as usual.')}
165
+
166
+ ${section('caching', 'Caching and performance')}
167
+ <p>Store fetchers run once per request, in parallel. They share the same request context as page server fetchers, so they can read cookies, params, and headers to scope data to the current user.</p>
168
+ <p>If your store data changes infrequently (nav items from a CMS, feature flags), consider adding a <code>serverTtl</code> to the relevant page spec to cache the full rendered HTML — or caching inside the fetcher itself:</p>
169
+ ${codeBlock(highlight(`// pulse.store.js — cache nav items in-process for 60 seconds
170
+ import { createCache } from './src/lib/cache.js'
171
+
172
+ const navCache = createCache(60)
173
+
174
+ export default {
175
+ server: {
176
+ nav: async () => navCache.getOrFetch('nav', () => cms.getNavItems()),
177
+ },
178
+ }`, 'js'))}
179
+ `,
180
+ }),
181
+ }
@@ -0,0 +1,108 @@
1
+ import { renderLayout, h1, lead, section, sub, codeBlock, callout, table } from '../lib/layout.js'
2
+ import { prevNext } from '../lib/nav.js'
3
+ import { highlight } from '../lib/highlight.js'
4
+
5
+ const { prev, next } = prevNext('/streaming')
6
+
7
+ export default {
8
+ route: '/streaming',
9
+ meta: {
10
+ title: 'Streaming SSR — Pulse Docs',
11
+ description: 'How streaming server-side rendering works in Pulse — shell, deferred segments, and when to use it.',
12
+ styles: ['/docs.css'],
13
+ },
14
+ state: {},
15
+ view: () => renderLayout({
16
+ currentHref: '/streaming',
17
+ prev,
18
+ next,
19
+ content: `
20
+ ${h1('Streaming SSR')}
21
+ ${lead('Streaming SSR eliminates the tradeoff between fast paint and real content. The shell — chrome, navigation, above-the-fold layout — renders and streams immediately. Slower data-dependent segments arrive over the same connection without blocking the initial paint.')}
22
+
23
+ ${section('how-it-works', 'How it works')}
24
+ <p>Without streaming, the server waits for all data to resolve before sending any HTML — slow queries block the entire response. Pulse splits the view into a <strong>shell</strong> (sent immediately) and <strong>deferred</strong> segments (sent as placeholders, then replaced when data resolves).</p>
25
+ <p>Deferred segments arrive as chunks of HTML over the same connection — no extra requests, no client-side JavaScript required to swap content in.</p>
26
+
27
+ ${section('enabling', 'Enabling streaming')}
28
+ <p>To use streaming, the <code>view</code> is an <strong>object of named segment functions</strong> rather than a single function. The spec declares which segments are in the shell and which are deferred:</p>
29
+ ${codeBlock(highlight(`export default {
30
+ route: '/dashboard',
31
+ state: {},
32
+ server: {
33
+ data: async (ctx) => ({
34
+ user: await auth.getUser(ctx.cookies.sessionId), // fast
35
+ feed: await db.feed.latest(), // slow
36
+ stats: await analytics.summary(ctx.params.id), // slow
37
+ }),
38
+ },
39
+ stream: {
40
+ shell: ['header', 'nav'], // rendered immediately
41
+ deferred: ['feed', 'stats'], // streamed when server data resolves
42
+ },
43
+ view: {
44
+ header: (state, server) => \`
45
+ <header class="site-header">
46
+ <a href="/">Dashboard</a>
47
+ <span>Hello, \${server.user.name}</span>
48
+ </header>
49
+ \`,
50
+ nav: () => \`
51
+ <nav>
52
+ <a href="/dashboard">Home</a>
53
+ <a href="/settings">Settings</a>
54
+ </nav>
55
+ \`,
56
+ feed: (state, server) => \`
57
+ <section class="feed">
58
+ \${server.feed.map(item => \`<article>\${item.title}</article>\`).join('')}
59
+ </section>
60
+ \`,
61
+ stats: (state, server) => \`
62
+ <div class="stats">
63
+ <p>Page views: \${server.stats.views}</p>
64
+ <p>Conversions: \${server.stats.conversions}</p>
65
+ </div>
66
+ \`,
67
+ },
68
+ }`, 'js'))}
69
+
70
+ ${section('placeholders', 'Deferred placeholders')}
71
+ <p>While deferred segments are loading, Pulse renders a <code>&lt;div id="pulse-slot-[name]"&gt;</code> placeholder in their place. When the segment resolves, the rendered HTML is appended to the stream and a small inline script swaps the placeholder content.</p>
72
+ ${callout('note', 'The swap is done with a tiny inline script — not a separate JS bundle. Deferred streaming works even on pages with no hydration (<code>hydrate</code> omitted).')}
73
+
74
+ ${section('server-data', 'Server data and streaming')}
75
+ <p>All <code>server.data()</code> calls resolve in a single async call before rendering begins. Streaming is about splitting the <em>view</em> — not about parallelising data fetching. For parallel data fetching, use <code>Promise.all</code> inside <code>server.data()</code>:</p>
76
+ ${codeBlock(highlight(`server: {
77
+ data: async (ctx) => {
78
+ // Fetch in parallel — both requests run concurrently
79
+ const [feed, stats] = await Promise.all([
80
+ db.feed.latest(),
81
+ analytics.summary(),
82
+ ])
83
+ return { feed, stats }
84
+ },
85
+ }`, 'js'))}
86
+
87
+ ${section('when-to-use', 'When to use streaming')}
88
+ ${table(
89
+ ['Scenario', 'Use streaming?'],
90
+ [
91
+ ['Page with fast server data (< 20ms)', 'No — standard SSR is simpler and fast enough'],
92
+ ['Page with slow database queries', 'Yes — stream the shell while data loads'],
93
+ ['Pages with above-the-fold and below-the-fold content', 'Yes — shell renders above the fold immediately'],
94
+ ['API or raw response endpoints', 'No — use <a href="/raw-responses">raw responses</a>'],
95
+ ]
96
+ )}
97
+
98
+ ${section('stream-option', 'Server-level streaming option')}
99
+ <p>Streaming is enabled by default in <code>createServer</code>. Disable it globally with <code>stream: false</code>:</p>
100
+ ${codeBlock(highlight(`createServer(specs, {
101
+ stream: false, // disable streaming for all specs
102
+ })`, 'js'))}
103
+ <p>Even with global streaming enabled, only specs that declare a <code>stream</code> field will use chunked responses. All other specs use regular buffered SSR.</p>
104
+
105
+ ${callout('tip', 'Streaming is most beneficial on pages with large datasets or slow queries. For most pages, the speed of Pulse\'s synchronous rendering means streaming adds unnecessary complexity.')}
106
+ `,
107
+ }),
108
+ }