@invisibleloop/pulse 0.20.11 → 0.20.20

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 (266) hide show
  1. package/package.json +6 -5
  2. package/public/.pulse-ui-version +1 -1
  3. package/scripts/build.js +26 -0
  4. package/scripts/release-version.js +17 -8
  5. package/scripts/strip-server.test.js +60 -0
  6. package/src/agent/checklist.md +8 -12
  7. package/src/agent/commands/pulse-start.md +2 -0
  8. package/src/agent/commands/verify.md +30 -8
  9. package/src/agent/guide-components.md +7 -25
  10. package/src/agent/guide-routing.md +0 -12
  11. package/src/agent/guide-server.md +102 -1
  12. package/src/agent/guide-spec.md +8 -1
  13. package/src/agent/guide-styles.md +23 -0
  14. package/src/agent/identity.md +9 -0
  15. package/src/agent/workflow.md +4 -23
  16. package/src/cli/check-bundles.js +113 -0
  17. package/src/cli/check-bundles.test.js +137 -0
  18. package/src/cli/cli.test.js +105 -0
  19. package/src/cli/dev.js +48 -3
  20. package/src/cli/diagnose.js +105 -0
  21. package/src/cli/diagnose.test.js +124 -0
  22. package/src/cli/discover.js +9 -2
  23. package/src/cli/index.js +171 -9
  24. package/src/cli/process-utils.js +57 -0
  25. package/src/cli/process-utils.test.js +114 -0
  26. package/src/cli/review.js +391 -0
  27. package/src/cli/review.test.js +143 -0
  28. package/src/cli/scaffold.js +9 -4
  29. package/src/cli/stamp.js +102 -0
  30. package/src/cli/stamp.test.js +149 -0
  31. package/src/cli/start.js +15 -2
  32. package/src/cli/validate.js +184 -0
  33. package/src/cli/validate.test.js +153 -0
  34. package/src/mcp/mcp.test.js +261 -30
  35. package/src/mcp/server.js +207 -515
  36. package/src/mcp/validate-worker.js +18 -1
  37. package/src/runtime/index.js +144 -11
  38. package/src/runtime/runtime.test.js +199 -0
  39. package/src/runtime/ssr.js +13 -6
  40. package/src/runtime/ssr.test.js +31 -0
  41. package/src/server/index.js +162 -9
  42. package/src/server/server.test.js +225 -2
  43. package/src/spec/schema.js +1 -1
  44. package/src/ui/index.js +0 -5
  45. package/src/ui/ui.test.js +0 -75
  46. package/types/ui.d.ts +0 -21
  47. package/.claude/commands/build-page.md +0 -63
  48. package/.claude/commands/new-doc-page.md +0 -45
  49. package/.claude/commands/pulse-start.md +0 -11
  50. package/.claude/commands/pulse-stop.md +0 -5
  51. package/.claude/commands/verify.md +0 -118
  52. package/.claude/pulse-checklist.md +0 -189
  53. package/.github/workflows/ci.yml +0 -25
  54. package/.github/workflows/publish.yml +0 -45
  55. package/.pulse/load-reports/home/1773432711417.json +0 -22
  56. package/.pulse-verified +0 -1
  57. package/AGENTS.md +0 -697
  58. package/CLAUDE.md +0 -697
  59. package/README.md +0 -95
  60. package/benchmark/public/dist/counter.boot-PO6SNSJZ.js +0 -8
  61. package/benchmark/public/dist/home.boot-YK6VAEQI.js +0 -6
  62. package/benchmark/public/dist/manifest.json +0 -5
  63. package/benchmark/public/dist/runtime-5Y7ESAIB.js +0 -2
  64. package/benchmark/public/dist/runtime-KO4BHUQ3.js +0 -49
  65. package/benchmark/results/2026-03-27-09-20.json +0 -188
  66. package/benchmark/results/2026-03-27-09-21.json +0 -188
  67. package/benchmark/results/2026-03-27-09-29.json +0 -188
  68. package/benchmark/results/2026-03-27-10-14.json +0 -188
  69. package/benchmark/results/2026-03-27-10-18.json +0 -232
  70. package/benchmark/src/pages/counter.js +0 -37
  71. package/benchmark/src/pages/home.js +0 -21
  72. package/docs/.claude/commands/pulse-build.md +0 -7
  73. package/docs/.claude/commands/pulse-dev.md +0 -7
  74. package/docs/.claude/commands/pulse-start.md +0 -9
  75. package/docs/.claude/commands/pulse-stop.md +0 -5
  76. package/docs/.claude/commands/verify.md +0 -84
  77. package/docs/.claude/coverage-check.js +0 -196
  78. package/docs/.claude/pulse-checklist.md +0 -189
  79. package/docs/public/.pulse-ui-version +0 -1
  80. package/docs/public/andypixel.css +0 -237
  81. package/docs/public/bookworms.css +0 -422
  82. package/docs/public/coutumes.css +0 -561
  83. package/docs/public/docs.css +0 -2388
  84. package/docs/public/favicon.svg +0 -4
  85. package/docs/public/home-brut.css +0 -982
  86. package/docs/public/images/bookworms/hero-books.jpg +0 -0
  87. package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
  88. package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
  89. package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
  90. package/docs/public/images/journey/178.jpg +0 -0
  91. package/docs/public/images/journey/29.jpg +0 -0
  92. package/docs/public/images/retrowheels/hero.jpg +0 -0
  93. package/docs/public/images/shop/devbook-pro.jpg +0 -0
  94. package/docs/public/images/suzukibento/1015.jpg +0 -0
  95. package/docs/public/images/suzukibento/1025.jpg +0 -0
  96. package/docs/public/images/suzukibento/1043.jpg +0 -0
  97. package/docs/public/images/suzukibento/1059.jpg +0 -0
  98. package/docs/public/images/suzukibento/1062.jpg +0 -0
  99. package/docs/public/images/suzukibento/1084.jpg +0 -0
  100. package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
  101. package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
  102. package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
  103. package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
  104. package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
  105. package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
  106. package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
  107. package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
  108. package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
  109. package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
  110. package/docs/public/img/heic1501a.jpg +0 -0
  111. package/docs/public/img/placeholder-landscape.svg +0 -18
  112. package/docs/public/img/placeholder-square.svg +0 -14
  113. package/docs/public/img/placeholder-tall.svg +0 -22
  114. package/docs/public/journey.css +0 -528
  115. package/docs/public/menu.js +0 -327
  116. package/docs/public/pulse-ui.css +0 -3028
  117. package/docs/public/pulse-ui.js +0 -257
  118. package/docs/public/retrowheels.css +0 -425
  119. package/docs/public/shop.css +0 -269
  120. package/docs/public/sparkys.css +0 -382
  121. package/docs/public/suzukibento.css +0 -517
  122. package/docs/public/swish.css +0 -394
  123. package/docs/public/theme.css +0 -52
  124. package/docs/public/themes/lumio.css +0 -50
  125. package/docs/public/themes/observatory.css +0 -104
  126. package/docs/server.js +0 -161
  127. package/docs/src/lib/component-page.js +0 -49
  128. package/docs/src/lib/highlight.js +0 -2
  129. package/docs/src/lib/layout.js +0 -190
  130. package/docs/src/lib/metrics-store.js +0 -6
  131. package/docs/src/lib/nav.js +0 -171
  132. package/docs/src/lib/stats.js +0 -81
  133. package/docs/src/pages/accessibility.js +0 -157
  134. package/docs/src/pages/actions.js +0 -194
  135. package/docs/src/pages/auth.js +0 -176
  136. package/docs/src/pages/caching.js +0 -95
  137. package/docs/src/pages/components/accordion.js +0 -48
  138. package/docs/src/pages/components/alert.js +0 -35
  139. package/docs/src/pages/components/app-badge.js +0 -41
  140. package/docs/src/pages/components/avatar.js +0 -35
  141. package/docs/src/pages/components/badge.js +0 -36
  142. package/docs/src/pages/components/breadcrumbs.js +0 -94
  143. package/docs/src/pages/components/button.js +0 -84
  144. package/docs/src/pages/components/card.js +0 -225
  145. package/docs/src/pages/components/charts.js +0 -283
  146. package/docs/src/pages/components/checkbox.js +0 -129
  147. package/docs/src/pages/components/cluster.js +0 -47
  148. package/docs/src/pages/components/container.js +0 -40
  149. package/docs/src/pages/components/cta.js +0 -54
  150. package/docs/src/pages/components/divider.js +0 -37
  151. package/docs/src/pages/components/empty.js +0 -36
  152. package/docs/src/pages/components/feature.js +0 -60
  153. package/docs/src/pages/components/fieldset.js +0 -65
  154. package/docs/src/pages/components/fileupload.js +0 -128
  155. package/docs/src/pages/components/footer.js +0 -58
  156. package/docs/src/pages/components/grid.js +0 -169
  157. package/docs/src/pages/components/heading.js +0 -107
  158. package/docs/src/pages/components/hero.js +0 -123
  159. package/docs/src/pages/components/icons.js +0 -410
  160. package/docs/src/pages/components/image.js +0 -71
  161. package/docs/src/pages/components/input.js +0 -51
  162. package/docs/src/pages/components/media.js +0 -51
  163. package/docs/src/pages/components/modal.js +0 -111
  164. package/docs/src/pages/components/nav.js +0 -136
  165. package/docs/src/pages/components/phone-frame.js +0 -74
  166. package/docs/src/pages/components/progress.js +0 -102
  167. package/docs/src/pages/components/prose.js +0 -111
  168. package/docs/src/pages/components/pullquote.js +0 -71
  169. package/docs/src/pages/components/radio.js +0 -194
  170. package/docs/src/pages/components/rating.js +0 -106
  171. package/docs/src/pages/components/search.js +0 -61
  172. package/docs/src/pages/components/section.js +0 -98
  173. package/docs/src/pages/components/segmented.js +0 -121
  174. package/docs/src/pages/components/select.js +0 -45
  175. package/docs/src/pages/components/slider.js +0 -115
  176. package/docs/src/pages/components/spinner.js +0 -73
  177. package/docs/src/pages/components/stack.js +0 -48
  178. package/docs/src/pages/components/stat.js +0 -55
  179. package/docs/src/pages/components/stepper.js +0 -66
  180. package/docs/src/pages/components/table.js +0 -45
  181. package/docs/src/pages/components/textarea.js +0 -31
  182. package/docs/src/pages/components/toggle.js +0 -84
  183. package/docs/src/pages/components/tooltip.js +0 -48
  184. package/docs/src/pages/components.js +0 -200
  185. package/docs/src/pages/config.js +0 -193
  186. package/docs/src/pages/constraints.js +0 -100
  187. package/docs/src/pages/deploy.js +0 -233
  188. package/docs/src/pages/error-pages.js +0 -99
  189. package/docs/src/pages/extending.js +0 -198
  190. package/docs/src/pages/faq.js +0 -99
  191. package/docs/src/pages/forms.js +0 -129
  192. package/docs/src/pages/getting-started.js +0 -147
  193. package/docs/src/pages/guard.js +0 -120
  194. package/docs/src/pages/home.js +0 -350
  195. package/docs/src/pages/how-it-works.js +0 -244
  196. package/docs/src/pages/hydration.js +0 -108
  197. package/docs/src/pages/images.js +0 -120
  198. package/docs/src/pages/markdown.js +0 -156
  199. package/docs/src/pages/meta.js +0 -193
  200. package/docs/src/pages/mutations.js +0 -107
  201. package/docs/src/pages/navigation.js +0 -85
  202. package/docs/src/pages/performance.js +0 -157
  203. package/docs/src/pages/persist.js +0 -89
  204. package/docs/src/pages/project-structure.js +0 -91
  205. package/docs/src/pages/prompt-examples.js +0 -186
  206. package/docs/src/pages/raw-responses.js +0 -122
  207. package/docs/src/pages/routing.js +0 -120
  208. package/docs/src/pages/server-api.js +0 -291
  209. package/docs/src/pages/server-data.js +0 -203
  210. package/docs/src/pages/slash-commands.js +0 -55
  211. package/docs/src/pages/spec.js +0 -207
  212. package/docs/src/pages/state.js +0 -102
  213. package/docs/src/pages/store.js +0 -203
  214. package/docs/src/pages/streaming.js +0 -107
  215. package/docs/src/pages/stripe.js +0 -194
  216. package/docs/src/pages/supabase.js +0 -324
  217. package/docs/src/pages/templates/andypixel.js +0 -260
  218. package/docs/src/pages/templates/blog-post.js +0 -226
  219. package/docs/src/pages/templates/bookworms.js +0 -227
  220. package/docs/src/pages/templates/coutumes.js +0 -237
  221. package/docs/src/pages/templates/journey.js +0 -186
  222. package/docs/src/pages/templates/mobile-app.js +0 -371
  223. package/docs/src/pages/templates/retrowheels.js +0 -355
  224. package/docs/src/pages/templates/shop.js +0 -268
  225. package/docs/src/pages/templates/sparkys.js +0 -264
  226. package/docs/src/pages/templates/suzukibento.js +0 -200
  227. package/docs/src/pages/templates/swish.js +0 -313
  228. package/docs/src/pages/templates.js +0 -534
  229. package/docs/src/pages/testing.js +0 -199
  230. package/docs/src/pages/validation.js +0 -138
  231. package/examples/.claude/commands/pulse-build.md +0 -7
  232. package/examples/.claude/commands/pulse-dev.md +0 -7
  233. package/examples/.claude/commands/pulse-start.md +0 -9
  234. package/examples/.claude/commands/pulse-stop.md +0 -5
  235. package/examples/.claude/commands/verify.md +0 -84
  236. package/examples/.claude/coverage-check.js +0 -196
  237. package/examples/.claude/pulse-checklist.md +0 -189
  238. package/examples/andypixel.css +0 -237
  239. package/examples/andypixel.js +0 -258
  240. package/examples/bookworms.css +0 -422
  241. package/examples/bookworms.js +0 -225
  242. package/examples/contact.js +0 -166
  243. package/examples/counter.js +0 -94
  244. package/examples/dev.server.js +0 -110
  245. package/examples/examples.test.js +0 -394
  246. package/examples/journey.css +0 -528
  247. package/examples/journey.js +0 -186
  248. package/examples/pricing.js +0 -262
  249. package/examples/products.js +0 -190
  250. package/examples/public/.pulse-ui-version +0 -1
  251. package/examples/public/pulse-ui.css +0 -3028
  252. package/examples/public/pulse-ui.js +0 -257
  253. package/examples/quiz.js +0 -208
  254. package/examples/retrowheels.css +0 -425
  255. package/examples/retrowheels.js +0 -353
  256. package/examples/shared.js +0 -84
  257. package/examples/suzukibento.css +0 -513
  258. package/examples/suzukibento.js +0 -200
  259. package/examples/swish.css +0 -394
  260. package/examples/swish.js +0 -311
  261. package/examples/todos.js +0 -162
  262. package/src/ui/fieldset.js +0 -37
  263. package/src/ui/phone-frame.js +0 -40
  264. package/src/ui/search.js +0 -78
  265. package/src/ui/segmented.js +0 -50
  266. package/src/ui/tooltip.js +0 -28
@@ -1,207 +0,0 @@
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
- theme: 'light',
11
- title: 'Spec Reference — Pulse Docs',
12
- description: 'Complete reference for every field in a Pulse page spec.',
13
- styles: ['/theme.css', '/docs.css'],
14
- },
15
- state: {},
16
- view: () => renderLayout({
17
- currentHref: '/spec',
18
- prev,
19
- next,
20
- content: `
21
- ${h1('Spec Reference')}
22
- ${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.')}
23
-
24
- ${section('quick-ref', 'Quick reference')}
25
- ${table(
26
- ['Field', 'Type', 'Required', 'Description'],
27
- [
28
- ['<code>route</code>', '<code>string</code>', 'Yes', 'URL pattern for this page. Supports <code>:param</code> segments.'],
29
- ['<code>state</code>', '<code>object</code>', 'No', 'Initial client-side state. Deep-cloned on mount. Defaults to <code>{}</code> — omit on purely server-rendered pages.'],
30
- ['<code>view</code>', '<code>function</code>', 'Yes', 'Returns an HTML string. Receives <code>(state, serverState)</code>.'],
31
- ['<code>meta</code>', '<code>object</code>', 'No', 'Page metadata: title, description, styles, OG tags, schema.'],
32
- ['<code>hydrate</code>', '<code>string</code>', 'Auto', 'Auto-derived from the URL entry in <code>createServer</code>. Do not set manually.'],
33
- ['<code>mutations</code>', '<code>object</code>', 'No', 'Synchronous state updaters keyed by name.'],
34
- ['<code>actions</code>', '<code>object</code>', 'No', 'Async operations with full lifecycle hooks.'],
35
- ['<code>validation</code>', '<code>object</code>', 'No', 'Declarative validation rules keyed by dot-path state keys.'],
36
- ['<code>constraints</code>', '<code>object</code>', 'No', 'Min/max bounds enforced after every mutation.'],
37
- ['<code>persist</code>', '<code>string[]</code>', 'No', 'State keys to save in <code>localStorage</code>.'],
38
- ['<code>server</code>', '<code>object</code>', 'No', 'Server-side data fetchers. Result passed to <code>view</code> as second arg. Use the <a href="/markdown"><code>md()</code> helper</a> to load <code>.md</code> files.'],
39
- ['<code>guard</code>', '<code>function</code>', 'No', 'Async pre-render hook. Return <code>{ redirect }</code> or <code>{ status, json }</code> to short-circuit. See <a href="/guard">Guard</a>.'],
40
- ['<code>store</code>', '<code>string[]</code>', 'No', 'Global store keys this page subscribes to. See <a href="/store">Global Store</a>.'],
41
- ['<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.'],
42
- ['<code>stream</code>', '<code>object</code>', 'No', 'Streaming SSR config: <code>shell</code> + <code>deferred</code> segment names.'],
43
- ['<code>cache</code>', '<code>object</code>', 'No', 'HTTP cache control headers for the page response.'],
44
- ['<code>serverTtl</code>', '<code>number</code>', 'No', 'Seconds to cache server data in-process.'],
45
- ['<code>serverTimeout</code>', '<code>number</code>', 'No', 'Timeout in ms for all server fetchers on this page. Overrides the global <code>fetcherTimeout</code> option.'],
46
- ['<code>contentType</code>', '<code>string</code>', 'No', 'Override response Content-Type. Enables raw (non-HTML) responses.'],
47
- ['<code>onViewError</code>', '<code>function</code>', 'No', 'Fallback renderer called when <code>view()</code> throws. Return an HTML string.'],
48
- ]
49
- )}
50
-
51
- ${section('route', 'route')}
52
- <p>The URL pattern this spec handles. Supports static segments and dynamic <code>:param</code> segments.</p>
53
- ${codeBlock(highlight(`route: '/products/:id' // matches /products/42
54
- route: '/blog/:year/:slug'`, 'js'))}
55
- <p>Dynamic segments are available in server data and actions via <code>ctx.params</code>. See <a href="/routing">Routing</a> for more.</p>
56
-
57
- ${section('state', 'state')}
58
- <p>The initial client-side state for the page. Always a plain object. Defaults to <code>{}</code> — you can omit it entirely on purely server-rendered pages with no <code>mutations</code>, <code>actions</code>, or <code>persist</code>. Pulse deep-clones it on every mount — mutations never affect the original spec, and state cannot leak between page loads.</p>
59
- ${codeBlock(highlight(`state: {
60
- count: 0,
61
- user: { name: '', email: '' },
62
- items: [],
63
- }`, 'js'))}
64
- <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>
65
-
66
- ${section('view', 'view')}
67
- <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>
68
- ${codeBlock(highlight(`view: (state, server) => \`
69
- <main>
70
- <h1>Hello, \${state.name}</h1>
71
- \${server.items.map(item => \`<p>\${item.title}</p>\`).join('')}
72
- </main>
73
- \``, 'js'))}
74
- <p>For streaming SSR, <code>view</code> can be an object of named segment functions. See <a href="/streaming">Streaming SSR</a>.</p>
75
-
76
- ${section('meta', 'meta')}
77
- <p>Page-level metadata. All fields are optional.</p>
78
- ${codeBlock(highlight(`meta: {
79
- title: 'Page Title — Site Name',
80
- description: 'Meta description for search engines.',
81
- styles: ['/app.css', '/page.css'],
82
- ogTitle: 'Open Graph title',
83
- ogImage: 'https://example.com/og.jpg',
84
- schema: { '@type': 'WebPage', name: 'Page Title' }, // ld+json
85
- }`, 'js'))}
86
- <p>See <a href="/meta">Metadata &amp; SEO</a> for the full reference.</p>
87
-
88
- ${section('hydrate', 'hydrate')}
89
- <p>Auto-derived by the framework — do not set this in your spec. When a spec is registered via a <code>URL</code> entry in <code>createServer</code>, Pulse detects whether it needs hydration (has <code>mutations</code>, <code>actions</code>, or <code>persist</code>) and sets the browser path automatically. Purely server-rendered specs get zero JavaScript.</p>
90
- ${callout('note', 'See <a href="/hydration">Hydration</a> for how this works and how to register specs via URL entries.')}
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
- }
@@ -1,102 +0,0 @@
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
- theme: 'light',
11
- title: 'State — Pulse Docs',
12
- description: 'How client state is declared, initialised, and used in Pulse.',
13
- styles: ['/theme.css', '/docs.css'],
14
- },
15
- state: {},
16
- view: () => renderLayout({
17
- currentHref: '/state',
18
- prev,
19
- next,
20
- content: `
21
- ${h1('State')}
22
- ${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.')}
23
-
24
- ${section('declaring', 'Declaring state')}
25
- <p>The <code>state</code> field of a spec is the initial value. Always a plain object — nested objects and arrays are supported:</p>
26
- ${codeBlock(highlight(`export default {
27
- route: '/checkout',
28
- state: {
29
- step: 1,
30
- customer: {
31
- name: '',
32
- email: '',
33
- },
34
- items: [],
35
- promoCode: null,
36
- },
37
- // ...
38
- }`, 'js'))}
39
- ${callout('note', '<code>state: {}</code> is always included, even on pages with no interactivity. The spec schema requires it.')}
40
-
41
- ${section('view-receives', 'The view receives state')}
42
- <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>
43
- ${codeBlock(highlight(`view: (state) => \`
44
- <div>
45
- <p>Step \${state.step} of 3</p>
46
- <p>Hello, \${state.customer.name || 'guest'}</p>
47
- <ul>
48
- \${state.items.map(item => \`<li>\${item.name}</li>\`).join('')}
49
- </ul>
50
- </div>
51
- \``, 'js'))}
52
-
53
- ${section('immutability', 'Immutability')}
54
- <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>
55
- ${codeBlock(highlight(`// CORRECT — return a partial update
56
- mutations: {
57
- nextStep: (state) => ({ step: state.step + 1 }),
58
- }
59
-
60
- // WRONG — never mutate state directly
61
- mutations: {
62
- nextStep: (state) => { state.step++ }, // ✗ do not do this
63
- }`, 'js'))}
64
- <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>
65
- ${codeBlock(highlight(`// state = { step: 1, customer: { name: 'Alice', email: 'a@b.com' } }
66
-
67
- mutations: {
68
- // Only updates step — customer is untouched
69
- nextStep: (state) => ({ step: state.step + 1 }),
70
-
71
- // Replaces the entire customer object — spread to preserve email
72
- setName: (state, e) => ({
73
- customer: { ...state.customer, name: e.target.value }
74
- }),
75
- }`, 'js'))}
76
-
77
- ${section('deep-clone', 'Deep clone on mount')}
78
- <p>When the page mounts, Pulse deep-clones <code>spec.state</code>. This guarantees:</p>
79
- <ul>
80
- <li>The live state and the spec's initial state are completely independent — mutations cannot corrupt the spec.</li>
81
- <li>Navigating away and back resets state to the spec's initial values (unless <a href="/persist">persisted</a>).</li>
82
- <li>Multiple instances of the same spec on the same page each get independent state.</li>
83
- </ul>
84
-
85
- ${section('server-state', 'State vs server state')}
86
- <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>
87
- ${codeBlock(highlight(`view: (state, server) => \`
88
- <div>
89
- <h1>\${server.product.name}</h1> <!-- server state -->
90
- <p>Qty: \${state.quantity}</p> <!-- client state -->
91
- </div>
92
- \``, 'js'))}
93
- ${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>.')}
94
-
95
- ${section('ssr-state', 'State during SSR')}
96
- <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>
97
-
98
- ${section('persist-link', 'Persisting state')}
99
- <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>
100
- `,
101
- }),
102
- }
@@ -1,203 +0,0 @@
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
- theme: 'light',
11
- title: 'Global Store — Pulse Docs',
12
- description: 'Share server-fetched data across pages with a global store in Pulse.',
13
- styles: ['/theme.css', '/docs.css'],
14
- },
15
- state: {},
16
- view: () => renderLayout({
17
- currentHref: '/store',
18
- prev,
19
- next,
20
- content: `
21
- ${h1('Global Store')}
22
- ${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.')}
23
-
24
- ${section('when-to-use', 'When to use the store')}
25
- <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>
26
- <ul>
27
- <li>Current user / session — <code>store.user</code></li>
28
- <li>App settings or feature flags — <code>store.settings</code></li>
29
- <li>Navigation items that come from a CMS — <code>store.nav</code></li>
30
- <li>Subscription or plan level — <code>store.plan</code></li>
31
- </ul>
32
- ${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.')}
33
-
34
- ${section('defining', 'Defining the store')}
35
- <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>
36
- ${codeBlock(highlight(`// pulse.store.js
37
- export default {
38
- // Default / fallback values used on the server before fetchers resolve
39
- state: {
40
- user: null,
41
- settings: { theme: 'dark', lang: 'en' },
42
- nav: [],
43
- },
44
-
45
- // Server fetchers — run once per request, results override state defaults
46
- server: {
47
- user: async (ctx) => db.users.findByCookie(ctx.cookies.session),
48
- settings: async (ctx) => db.settings.forUser(ctx.cookies.userId),
49
- nav: async () => cms.getNavItems(),
50
- },
51
- }`, 'js'))}
52
-
53
- ${section('registering', 'Registering the store')}
54
- <p><strong>There is no registration step.</strong> <code>pulse dev</code> and <code>pulse start</code> auto-discover <code>pulse.store.js</code> from the project root — the same way pages are discovered from <code>src/pages/</code>. The store is loaded server-side, served to the browser for mutations, validated at startup (bad fetchers fail the boot), and hot-reloaded in dev when you edit it.</p>
55
- <p>Only when you run your own server entry do you pass it manually:</p>
56
- ${codeBlock(highlight(`import { createServer } from '@invisibleloop/pulse'
57
- import store from './pulse.store.js'
58
-
59
- createServer(pages, {
60
- port: 3000,
61
- staticDir: 'public',
62
- store, // ← manual server entry only
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('live', 'Live store push — server-initiated updates')}
167
- <p>For data that changes on the <strong>server</strong> — stock levels, live scores, announcements — enable the live channel and broadcast with <code>pushStore</code>. Subscribed pages re-render the moment a patch arrives, with no polling and no client code:</p>
168
- ${codeBlock(highlight(`// pulse.config.js — enable the channel
169
- export default { live: true }`, 'js'))}
170
- ${codeBlock(highlight(`// Broadcast from any spec file — e.g. a webhook that updates stock:
171
- import { pushStore } from '@invisibleloop/pulse'
172
-
173
- export default {
174
- route: '/hooks/stock',
175
- contentType: 'text/plain',
176
- state: {},
177
- render: async (ctx) => {
178
- const { stock } = await ctx.json()
179
- pushStore({ stock }) // pages with store: ['stock'] re-render
180
- return 'ok'
181
- },
182
- }`, 'js'))}
183
- <p>When you own the server entry, <code>createServer</code> also returns the handle directly: <code>const { pushStore } = await createServer(pages, { live: true })</code>.</p>
184
- <p>The browser side is automatic: pages that declare <code>store: [...]</code> keys open a single <code>EventSource</code> connection per tab (default endpoint <code>/__pulse/live</code>). The connection survives client-side navigations and reconnects natively after network interruptions. Pages without store keys never connect.</p>
185
- ${callout('warning', 'The live channel is a <strong>broadcast</strong> — every connected client receives every patch. Push <strong>shared</strong> data only (stock, announcements, scores). For per-user updates, use page actions with <code>_storeUpdate</code> instead.')}
186
- ${callout('note', 'Views must handle keys that haven\'t arrived yet — the first patch may land seconds after page load. Write <code>server.stock ?? \'—\'</code>, not <code>server.stock</code>. The page must also be hydrated (have mutations or actions); purely server-rendered pages have no JavaScript to receive patches.')}
187
-
188
- ${section('caching', 'Caching and performance')}
189
- <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>
190
- <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>
191
- ${codeBlock(highlight(`// pulse.store.js — cache nav items in-process for 60 seconds
192
- import { createCache } from './src/lib/cache.js'
193
-
194
- const navCache = createCache(60)
195
-
196
- export default {
197
- server: {
198
- nav: async () => navCache.getOrFetch('nav', () => cms.getNavItems()),
199
- },
200
- }`, 'js'))}
201
- `,
202
- }),
203
- }
@@ -1,107 +0,0 @@
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
- theme: 'light',
11
- title: 'Streaming SSR — Pulse Docs',
12
- description: 'How streaming server-side rendering works in Pulse — shell, deferred segments, and when to use it.',
13
- styles: ['/theme.css', '/docs.css'],
14
- },
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
- server: {
32
- data: async (ctx) => ({
33
- user: await auth.getUser(ctx.cookies.sessionId), // fast
34
- feed: await db.feed.latest(), // slow
35
- stats: await analytics.summary(ctx.params.id), // slow
36
- }),
37
- },
38
- stream: {
39
- shell: ['header', 'nav'], // rendered immediately
40
- deferred: ['feed', 'stats'], // streamed when server data resolves
41
- },
42
- view: {
43
- header: (state, server) => \`
44
- <header class="site-header">
45
- <a href="/">Dashboard</a>
46
- <span>Hello, \${server.user.name}</span>
47
- </header>
48
- \`,
49
- nav: () => \`
50
- <nav>
51
- <a href="/dashboard">Home</a>
52
- <a href="/settings">Settings</a>
53
- </nav>
54
- \`,
55
- feed: (state, server) => \`
56
- <section class="feed">
57
- \${server.feed.map(item => \`<article>\${item.title}</article>\`).join('')}
58
- </section>
59
- \`,
60
- stats: (state, server) => \`
61
- <div class="stats">
62
- <p>Page views: \${server.stats.views}</p>
63
- <p>Conversions: \${server.stats.conversions}</p>
64
- </div>
65
- \`,
66
- },
67
- }`, 'js'))}
68
-
69
- ${section('placeholders', 'Deferred placeholders')}
70
- <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>
71
- ${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).')}
72
-
73
- ${section('server-data', 'Server data and streaming')}
74
- <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>
75
- ${codeBlock(highlight(`server: {
76
- data: async (ctx) => {
77
- // Fetch in parallel — both requests run concurrently
78
- const [feed, stats] = await Promise.all([
79
- db.feed.latest(),
80
- analytics.summary(),
81
- ])
82
- return { feed, stats }
83
- },
84
- }`, 'js'))}
85
-
86
- ${section('when-to-use', 'When to use streaming')}
87
- ${table(
88
- ['Scenario', 'Use streaming?'],
89
- [
90
- ['Page with fast server data (< 20ms)', 'No — standard SSR is simpler and fast enough'],
91
- ['Page with slow database queries', 'Yes — stream the shell while data loads'],
92
- ['Pages with above-the-fold and below-the-fold content', 'Yes — shell renders above the fold immediately'],
93
- ['API or raw response endpoints', 'No — use <a href="/raw-responses">raw responses</a>'],
94
- ]
95
- )}
96
-
97
- ${section('stream-option', 'Server-level streaming option')}
98
- <p>Streaming is enabled by default in <code>createServer</code>. Disable it globally with <code>stream: false</code>:</p>
99
- ${codeBlock(highlight(`createServer(specs, {
100
- stream: false, // disable streaming for all specs
101
- })`, 'js'))}
102
- <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>
103
-
104
- ${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.')}
105
- `,
106
- }),
107
- }