@invisibleloop/pulse 0.1.28 → 0.1.29

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 (117) hide show
  1. package/.claude/settings.local.json +113 -0
  2. package/.github/workflows/publish.yml +11 -21
  3. package/docs/public/.pulse-ui-version +1 -1
  4. package/docs/public/docs.css +19 -1
  5. package/docs/public/pulse-ui.css +1 -0
  6. package/docs/server.js +5 -2
  7. package/docs/src/lib/highlight.js +57 -13
  8. package/docs/src/lib/layout.js +5 -2
  9. package/docs/src/pages/faq.js +5 -2
  10. package/docs/src/pages/home.js +9 -5
  11. package/docs/src/pages/meta.js +21 -0
  12. package/docs/src/pages/routing.js +12 -1
  13. package/package.json +1 -1
  14. package/src/agent/guide-routing.md +20 -0
  15. package/src/agent/guide-spec.md +9 -1
  16. package/src/cli/scaffold.js +63 -2
  17. package/src/server/index.js +21 -6
  18. package/src/server/server.test.js +47 -0
  19. package/docs/public/dist/accessibility.boot-5DVTARJU.js +0 -115
  20. package/docs/public/dist/actions.boot-P66HKQEM.js +0 -164
  21. package/docs/public/dist/auth.boot-IMAJAUPH.js +0 -140
  22. package/docs/public/dist/caching.boot-DVR6KDE7.js +0 -53
  23. package/docs/public/dist/components--accordion.boot-3HVKMNWC.js +0 -11
  24. package/docs/public/dist/components--alert.boot-GCEXOZAC.js +0 -6
  25. package/docs/public/dist/components--app-badge.boot-DVT3GCHJ.js +0 -6
  26. package/docs/public/dist/components--avatar.boot-PSW24EVA.js +0 -5
  27. package/docs/public/dist/components--badge.boot-TYDY2RMK.js +0 -7
  28. package/docs/public/dist/components--banner.boot-EI5PZSZK.js +0 -7
  29. package/docs/public/dist/components--breadcrumbs.boot-SMA2E2GO.js +0 -34
  30. package/docs/public/dist/components--button.boot-J54BQM2E.js +0 -23
  31. package/docs/public/dist/components--card.boot-PZGNDIB6.js +0 -138
  32. package/docs/public/dist/components--carousel.boot-TP6LPFZZ.js +0 -12
  33. package/docs/public/dist/components--charts.boot-2EOYQWKL.js +0 -108
  34. package/docs/public/dist/components--checkbox.boot-DS5BSL6T.js +0 -54
  35. package/docs/public/dist/components--cluster.boot-HHVIBBJG.js +0 -9
  36. package/docs/public/dist/components--code-window.boot-2GR2DV33.js +0 -20
  37. package/docs/public/dist/components--container.boot-7LOOGK2K.js +0 -5
  38. package/docs/public/dist/components--cta.boot-FSNZ5YRT.js +0 -11
  39. package/docs/public/dist/components--divider.boot-3NI2C3QG.js +0 -6
  40. package/docs/public/dist/components--empty.boot-YX2UR3PV.js +0 -7
  41. package/docs/public/dist/components--feature.boot-MUD7NSUO.js +0 -13
  42. package/docs/public/dist/components--fieldset.boot-J7BYHMKF.js +0 -19
  43. package/docs/public/dist/components--fileupload.boot-NIKVTTPD.js +0 -52
  44. package/docs/public/dist/components--footer.boot-EYUK5FRG.js +0 -14
  45. package/docs/public/dist/components--grid.boot-URDQVDDR.js +0 -59
  46. package/docs/public/dist/components--heading.boot-BPQKU43E.js +0 -44
  47. package/docs/public/dist/components--hero.boot-4RAPRGAB.js +0 -17
  48. package/docs/public/dist/components--icons.boot-ZITNU5JP.js +0 -68
  49. package/docs/public/dist/components--image.boot-XEEGHQZF.js +0 -19
  50. package/docs/public/dist/components--input.boot-SGASZG5K.js +0 -7
  51. package/docs/public/dist/components--list.boot-W3XC5MHD.js +0 -55
  52. package/docs/public/dist/components--media.boot-5VFIETZO.js +0 -13
  53. package/docs/public/dist/components--modal.boot-RZUYXBN2.js +0 -47
  54. package/docs/public/dist/components--nav.boot-ODBOHU7O.js +0 -33
  55. package/docs/public/dist/components--pricing.boot-4AQ4ZVBY.js +0 -21
  56. package/docs/public/dist/components--progress.boot-GHAGYZOK.js +0 -30
  57. package/docs/public/dist/components--prose.boot-QANJL6JI.js +0 -67
  58. package/docs/public/dist/components--pullquote.boot-Q2WMNAZU.js +0 -22
  59. package/docs/public/dist/components--radio.boot-TJRDQ2OL.js +0 -75
  60. package/docs/public/dist/components--rating.boot-QBAN6DEL.js +0 -38
  61. package/docs/public/dist/components--search.boot-PXH5O5AG.js +0 -17
  62. package/docs/public/dist/components--section.boot-AQGIYHWW.js +0 -12
  63. package/docs/public/dist/components--segmented.boot-BEVTKEJO.js +0 -33
  64. package/docs/public/dist/components--select.boot-47X5RHOC.js +0 -10
  65. package/docs/public/dist/components--slider.boot-PSRRX7XL.js +0 -47
  66. package/docs/public/dist/components--spinner.boot-MZ5MO2OH.js +0 -22
  67. package/docs/public/dist/components--stack.boot-DI4NJXBF.js +0 -9
  68. package/docs/public/dist/components--stat.boot-QMFUWBQT.js +0 -9
  69. package/docs/public/dist/components--stepper.boot-34PP2NEV.js +0 -22
  70. package/docs/public/dist/components--table.boot-FCQGSFIQ.js +0 -11
  71. package/docs/public/dist/components--testimonial.boot-DWQPDKYG.js +0 -11
  72. package/docs/public/dist/components--textarea.boot-QVXLBOJ5.js +0 -4
  73. package/docs/public/dist/components--timeline.boot-26LN52P2.js +0 -95
  74. package/docs/public/dist/components--toggle.boot-IQQEI76S.js +0 -29
  75. package/docs/public/dist/components--tooltip.boot-LGHCO6NN.js +0 -9
  76. package/docs/public/dist/components.boot-SE6PQ4P7.js +0 -103
  77. package/docs/public/dist/config.boot-DTRRWUE6.js +0 -126
  78. package/docs/public/dist/constraints.boot-DUHDZBMC.js +0 -71
  79. package/docs/public/dist/deploy.boot-SLAD3NI2.js +0 -163
  80. package/docs/public/dist/docs-8e3d4b5c.css +0 -1
  81. package/docs/public/dist/extending.boot-UA3CN243.js +0 -159
  82. package/docs/public/dist/faq.boot-6EQAWLQR.js +0 -43
  83. package/docs/public/dist/getting-started.boot-TDKIFL5U.js +0 -86
  84. package/docs/public/dist/guard.boot-AUHAWTG4.js +0 -80
  85. package/docs/public/dist/home.boot-BVQXRH32.js +0 -383
  86. package/docs/public/dist/how-it-works.boot-LTWAKWKW.js +0 -104
  87. package/docs/public/dist/hydration.boot-JRM6IPJL.js +0 -78
  88. package/docs/public/dist/images.boot-M6ZVKTZS.js +0 -80
  89. package/docs/public/dist/manifest.json +0 -94
  90. package/docs/public/dist/meta.boot-7NXGPHR4.js +0 -79
  91. package/docs/public/dist/mutations.boot-F6F43UDX.js +0 -79
  92. package/docs/public/dist/navigation.boot-AOXWS3ZF.js +0 -57
  93. package/docs/public/dist/performance.boot-C3UPCOBK.js +0 -98
  94. package/docs/public/dist/persist.boot-WT32PQOQ.js +0 -61
  95. package/docs/public/dist/project-structure.boot-FB3LRVJ4.js +0 -63
  96. package/docs/public/dist/prompt-examples.boot-YKR4VDK4.js +0 -31
  97. package/docs/public/dist/pulse-ui-81a85c03.css +0 -1
  98. package/docs/public/dist/raw-responses.boot-M4KA5YXL.js +0 -104
  99. package/docs/public/dist/routing.boot-FNX5FDGH.js +0 -70
  100. package/docs/public/dist/runtime-B73WLANC.js +0 -1
  101. package/docs/public/dist/runtime-KO4BHUQ3.js +0 -49
  102. package/docs/public/dist/runtime-L2HNXIHW.js +0 -59
  103. package/docs/public/dist/runtime-QFURDKA2.js +0 -5
  104. package/docs/public/dist/runtime-UVPXO4IR.js +0 -375
  105. package/docs/public/dist/runtime-VMJA3Z4N.js +0 -10
  106. package/docs/public/dist/runtime-ZJ4FXT5O.js +0 -11
  107. package/docs/public/dist/server-api.boot-K7X3LCFB.js +0 -219
  108. package/docs/public/dist/server-data.boot-Y7HQYC4R.js +0 -157
  109. package/docs/public/dist/slash-commands.boot-V2UV7OW2.js +0 -26
  110. package/docs/public/dist/spec.boot-2WU7ZHCV.js +0 -159
  111. package/docs/public/dist/state.boot-B24GUE3R.js +0 -73
  112. package/docs/public/dist/store.boot-TLIB4XHH.js +0 -150
  113. package/docs/public/dist/streaming.boot-W2DZSMW4.js +0 -80
  114. package/docs/public/dist/stripe.boot-QN3C2GEL.js +0 -164
  115. package/docs/public/dist/supabase.boot-BG4XXLZE.js +0 -303
  116. package/docs/public/dist/testing.boot-6U4WKMTE.js +0 -130
  117. package/docs/public/dist/validation.boot-PQHYGW5B.js +0 -100
@@ -1,73 +0,0 @@
1
- import{a as s}from"./runtime-QFURDKA2.js";import{a as o,b as d,c,d as l,e,g as t,i}from"./runtime-L2HNXIHW.js";import{a as r,b as p}from"./runtime-B73WLANC.js";var{prev:u,next:m}=o("/state"),n={route:"/state",meta:{title:"State \u2014 Pulse Docs",description:"How client state is declared, initialised, and used in Pulse.",styles:["/docs.css"]},state:{},view:()=>d({currentHref:"/state",prev:u,next:m,content:`
2
- ${c("State")}
3
- ${l("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 \u2014 the framework prevents it by design.")}
4
-
5
- ${e("declaring","Declaring state")}
6
- <p>The <code>state</code> field of a spec is the initial value. Always a plain object \u2014 nested objects and arrays are supported:</p>
7
- ${t(s(`export default {
8
- route: '/checkout',
9
- state: {
10
- step: 1,
11
- customer: {
12
- name: '',
13
- email: '',
14
- },
15
- items: [],
16
- promoCode: null,
17
- },
18
- // ...
19
- }`,"js"))}
20
- ${i("note","<code>state: {}</code> is always included, even on pages with no interactivity. The spec schema requires it.")}
21
-
22
- ${e("view-receives","The view receives state")}
23
- <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>
24
- ${t(s("view: (state) => `\n <div>\n <p>Step ${state.step} of 3</p>\n <p>Hello, ${state.customer.name || 'guest'}</p>\n <ul>\n ${state.items.map(item => `<li>${item.name}</li>`).join('')}\n </ul>\n </div>\n`","js"))}
25
-
26
- ${e("immutability","Immutability")}
27
- <p>State is never mutated directly. Mutations are pure functions that return a <em>partial</em> object to merge \u2014 the framework rejects any other pattern:</p>
28
- ${t(s(`// CORRECT \u2014 return a partial update
29
- mutations: {
30
- nextStep: (state) => ({ step: state.step + 1 }),
31
- }
32
-
33
- // WRONG \u2014 never mutate state directly
34
- mutations: {
35
- nextStep: (state) => { state.step++ }, // \u2717 do not do this
36
- }`,"js"))}
37
- <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>
38
- ${t(s(`// state = { step: 1, customer: { name: 'Alice', email: 'a@b.com' } }
39
-
40
- mutations: {
41
- // Only updates step \u2014 customer is untouched
42
- nextStep: (state) => ({ step: state.step + 1 }),
43
-
44
- // Replaces the entire customer object \u2014 spread to preserve email
45
- setName: (state, e) => ({
46
- customer: { ...state.customer, name: e.target.value }
47
- }),
48
- }`,"js"))}
49
-
50
- ${e("deep-clone","Deep clone on mount")}
51
- <p>When the page mounts, Pulse deep-clones <code>spec.state</code>. This guarantees:</p>
52
- <ul>
53
- <li>The live state and the spec's initial state are completely independent \u2014 mutations cannot corrupt the spec.</li>
54
- <li>Navigating away and back resets state to the spec's initial values (unless <a href="/persist">persisted</a>).</li>
55
- <li>Multiple instances of the same spec on the same page each get independent state.</li>
56
- </ul>
57
-
58
- ${e("server-state","State vs server state")}
59
- <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 \u2014 it is never exposed to the client after hydration:</p>
60
- ${t(s(`view: (state, server) => \`
61
- <div>
62
- <h1>\${server.product.name}</h1> <!-- server state -->
63
- <p>Qty: \${state.quantity}</p> <!-- client state -->
64
- </div>
65
- \``,"js"))}
66
- ${i("note","Server state is read-only and not available on the client after hydration. Anything that needs client interactivity belongs in <code>state</code>.")}
67
-
68
- ${e("ssr-state","State during SSR")}
69
- <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 \u2014 the initial HTML arrives from the server and the JS binds events without touching the DOM.</p>
70
-
71
- ${e("persist-link","Persisting state")}
72
- <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>
73
- `})};var a=document.getElementById("pulse-root");a&&!a.dataset.pulseMounted&&(a.dataset.pulseMounted="1",r(n,a,window.__PULSE_SERVER__||{},{ssr:!0}),p(a,r));var T=n;export{T as default};
@@ -1,150 +0,0 @@
1
- import{a as s}from"./runtime-QFURDKA2.js";import{a as n,b as d,c as l,d as h,e,g as t,h as a,i as o}from"./runtime-L2HNXIHW.js";import{a as c,b as p}from"./runtime-B73WLANC.js";var{prev:u,next:f}=n("/store"),i={route:"/store",meta:{title:"Global Store \u2014 Pulse Docs",description:"Share server-fetched data across pages with a global store in Pulse.",styles:["/docs.css"]},state:{},view:()=>d({currentHref:"/store",prev:u,next:f,content:`
2
- ${l("Global Store")}
3
- ${h("The global store is a single shared data layer. Declare server fetchers once in <code>pulse.store.js</code> \u2014 user profiles, settings, feature flags \u2014 and any page can access them by name. No prop drilling, no repeated fetches.")}
4
-
5
- ${e("when-to-use","When to use the store")}
6
- <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>
7
- <ul>
8
- <li>Current user / session \u2014 <code>store.user</code></li>
9
- <li>App settings or feature flags \u2014 <code>store.settings</code></li>
10
- <li>Navigation items that come from a CMS \u2014 <code>store.nav</code></li>
11
- <li>Subscription or plan level \u2014 <code>store.plan</code></li>
12
- </ul>
13
- ${o("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.')}
14
-
15
- ${e("defining","Defining the store")}
16
- <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>
17
- ${t(s(`// pulse.store.js
18
- export default {
19
- // Default / fallback values used on the server before fetchers resolve
20
- state: {
21
- user: null,
22
- settings: { theme: 'dark', lang: 'en' },
23
- nav: [],
24
- },
25
-
26
- // Server fetchers \u2014 run once per request, results override state defaults
27
- server: {
28
- user: async (ctx) => db.users.findByCookie(ctx.cookies.session),
29
- settings: async (ctx) => db.settings.forUser(ctx.cookies.userId),
30
- nav: async () => cms.getNavItems(),
31
- },
32
- }`,"js"))}
33
-
34
- ${e("registering","Registering the store")}
35
- <p>Pass your store to <code>createServer</code> via the <code>store</code> option. The store is validated at startup \u2014 bad fetchers throw before the server accepts connections:</p>
36
- ${t(s(`import { createServer } from '@invisibleloop/pulse'
37
- import store from './pulse.store.js'
38
- import { dashboardSpec } from './src/pages/dashboard.js'
39
- import { settingsSpec } from './src/pages/settings.js'
40
-
41
- createServer([dashboardSpec, settingsSpec], {
42
- port: 3000,
43
- staticDir: 'public',
44
- store, // \u2190 register the global store
45
- })`,"js"))}
46
-
47
- ${e("using","Using store data in a page")}
48
- <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 \u2014 alongside any page-level server data:</p>
49
- ${t(s(`// src/pages/dashboard.js
50
- export default {
51
- route: '/dashboard',
52
- store: ['user', 'settings'], // declare which store keys this page uses
53
-
54
- // Page-level server data still works alongside store data
55
- server: {
56
- stats: async (ctx) => db.stats.forUser(ctx.store.user?.id),
57
- },
58
-
59
- state: { filter: 'week' },
60
-
61
- view: (state, server) => \`
62
- <main>
63
- <h1>Hello, \${server.user?.name ?? 'there'}</h1>
64
- <p>Theme: \${server.settings.theme}</p>
65
- <p>Stats: \${server.stats.total} requests this \${state.filter}</p>
66
- </main>
67
- \`,
68
- }`,"js"))}
69
- <p>Only the keys listed in <code>spec.store</code> are available in the view \u2014 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>
70
-
71
- ${e("ctx-store","Accessing the store in server fetchers")}
72
- <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>
73
- ${t(s(`export default {
74
- route: '/account',
75
- store: ['user'],
76
-
77
- // Guard can use ctx.store to check auth before fetching page data
78
- guard: async (ctx) => {
79
- if (!ctx.store.user) return { redirect: '/login' }
80
- },
81
-
82
- // Server fetchers receive ctx.store with the resolved store state
83
- server: {
84
- orders: async (ctx) => db.orders.forUser(ctx.store.user.id),
85
- },
86
-
87
- view: (state, server) => \`
88
- <h1>Orders for \${server.user.name}</h1>
89
- \`,
90
- }`,"js"))}
91
-
92
- ${e("store-field-reference","Store field reference")}
93
- ${a(["Field","Type","Description"],[["<code>state</code>","object","Default values. Used as fallbacks when a server fetcher returns <code>undefined</code> or the server key is absent."],["<code>server</code>","object of functions","Async fetchers \u2014 <code>async (ctx) => value</code>. Receive the same <code>ctx</code> as page server fetchers. Results override <code>state</code> defaults."]])}
94
-
95
- ${e("spec-store-reference","spec.store field")}
96
- ${a(["Field","Type","Description"],[["<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>"]])}
97
- ${o("tip","Pages that do not declare <code>store</code> receive no store data \u2014 the store never leaks to pages that do not ask for it.")}
98
-
99
- ${e("reactivity","Reactive updates \u2014 no refresh needed")}
100
- <p>When a page action changes store data, all other mounted pages that subscribe to the affected keys re-render immediately \u2014 no page refresh, no polling.</p>
101
- <p>Return <code>_storeUpdate</code> from a page action's <code>onSuccess</code> to push a change into the global store:</p>
102
- ${t(s(`// src/pages/settings.js
103
- export default {
104
- route: '/settings',
105
- store: ['settings'],
106
- state: { saved: false },
107
-
108
- actions: {
109
- saveTheme: {
110
- run: async (state, server, payload) => {
111
- const theme = payload.get('theme')
112
- await fetch('/api/settings', { method: 'PATCH', body: payload })
113
- return theme
114
- },
115
- onSuccess: (state, theme) => ({
116
- saved: true,
117
- _storeUpdate: { settings: { theme } }, // \u2190 push to global store
118
- }),
119
- onError: (state, err) => ({ error: err.message }),
120
- },
121
- },
122
-
123
- view: (state, server) => \`
124
- <form data-action="saveTheme">
125
- <select name="theme">
126
- <option value="dark" \${server.settings.theme === 'dark' ? 'selected' : ''}>Dark</option>
127
- <option value="light" \${server.settings.theme === 'light' ? 'selected' : ''}>Light</option>
128
- </select>
129
- <button type="submit">Save</button>
130
- \${state.saved ? '<p>Saved!</p>' : ''}
131
- </form>
132
- \`,
133
- }`,"js"))}
134
- <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 \u2014 without navigating away or refreshing.</p>
135
- ${o("note","<code>_storeUpdate</code> is stripped from the local page state \u2014 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.")}
136
-
137
- ${e("caching","Caching and performance")}
138
- <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>
139
- <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 \u2014 or caching inside the fetcher itself:</p>
140
- ${t(s(`// pulse.store.js \u2014 cache nav items in-process for 60 seconds
141
- import { createCache } from './src/lib/cache.js'
142
-
143
- const navCache = createCache(60)
144
-
145
- export default {
146
- server: {
147
- nav: async () => navCache.getOrFetch('nav', () => cms.getNavItems()),
148
- },
149
- }`,"js"))}
150
- `})};var r=document.getElementById("pulse-root");r&&!r.dataset.pulseMounted&&(r.dataset.pulseMounted="1",c(i,r,window.__PULSE_SERVER__||{},{ssr:!0}),p(r,c));var x=i;export{x as default};
@@ -1,80 +0,0 @@
1
- import{a}from"./runtime-QFURDKA2.js";import{a as i,b as d,c as l,d as c,e,g as t,h,i as r}from"./runtime-L2HNXIHW.js";import{a as n,b as m}from"./runtime-B73WLANC.js";var{prev:p,next:g}=i("/streaming"),o={route:"/streaming",meta:{title:"Streaming SSR \u2014 Pulse Docs",description:"How streaming server-side rendering works in Pulse \u2014 shell, deferred segments, and when to use it.",styles:["/docs.css"]},state:{},view:()=>d({currentHref:"/streaming",prev:p,next:g,content:`
2
- ${l("Streaming SSR")}
3
- ${c("Streaming SSR eliminates the tradeoff between fast paint and real content. The shell \u2014 chrome, navigation, above-the-fold layout \u2014 renders and streams immediately. Slower data-dependent segments arrive over the same connection without blocking the initial paint.")}
4
-
5
- ${e("how-it-works","How it works")}
6
- <p>Without streaming, the server waits for all data to resolve before sending any HTML \u2014 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>
7
- <p>Deferred segments arrive as chunks of HTML over the same connection \u2014 no extra requests, no client-side JavaScript required to swap content in.</p>
8
-
9
- ${e("enabling","Enabling streaming")}
10
- <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>
11
- ${t(a(`export default {
12
- route: '/dashboard',
13
- state: {},
14
- server: {
15
- data: async (ctx) => ({
16
- user: await auth.getUser(ctx.cookies.sessionId), // fast
17
- feed: await db.feed.latest(), // slow
18
- stats: await analytics.summary(ctx.params.id), // slow
19
- }),
20
- },
21
- stream: {
22
- shell: ['header', 'nav'], // rendered immediately
23
- deferred: ['feed', 'stats'], // streamed when server data resolves
24
- },
25
- view: {
26
- header: (state, server) => \`
27
- <header class="site-header">
28
- <a href="/">Dashboard</a>
29
- <span>Hello, \${server.user.name}</span>
30
- </header>
31
- \`,
32
- nav: () => \`
33
- <nav>
34
- <a href="/dashboard">Home</a>
35
- <a href="/settings">Settings</a>
36
- </nav>
37
- \`,
38
- feed: (state, server) => \`
39
- <section class="feed">
40
- \${server.feed.map(item => \`<article>\${item.title}</article>\`).join('')}
41
- </section>
42
- \`,
43
- stats: (state, server) => \`
44
- <div class="stats">
45
- <p>Page views: \${server.stats.views}</p>
46
- <p>Conversions: \${server.stats.conversions}</p>
47
- </div>
48
- \`,
49
- },
50
- }`,"js"))}
51
-
52
- ${e("placeholders","Deferred placeholders")}
53
- <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>
54
- ${r("note","The swap is done with a tiny inline script \u2014 not a separate JS bundle. Deferred streaming works even on pages with no hydration (<code>hydrate</code> omitted).")}
55
-
56
- ${e("server-data","Server data and streaming")}
57
- <p>All <code>server.data()</code> calls resolve in a single async call before rendering begins. Streaming is about splitting the <em>view</em> \u2014 not about parallelising data fetching. For parallel data fetching, use <code>Promise.all</code> inside <code>server.data()</code>:</p>
58
- ${t(a(`server: {
59
- data: async (ctx) => {
60
- // Fetch in parallel \u2014 both requests run concurrently
61
- const [feed, stats] = await Promise.all([
62
- db.feed.latest(),
63
- analytics.summary(),
64
- ])
65
- return { feed, stats }
66
- },
67
- }`,"js"))}
68
-
69
- ${e("when-to-use","When to use streaming")}
70
- ${h(["Scenario","Use streaming?"],[["Page with fast server data (< 20ms)","No \u2014 standard SSR is simpler and fast enough"],["Page with slow database queries","Yes \u2014 stream the shell while data loads"],["Pages with above-the-fold and below-the-fold content","Yes \u2014 shell renders above the fold immediately"],["API or raw response endpoints",'No \u2014 use <a href="/raw-responses">raw responses</a>']])}
71
-
72
- ${e("stream-option","Server-level streaming option")}
73
- <p>Streaming is enabled by default in <code>createServer</code>. Disable it globally with <code>stream: false</code>:</p>
74
- ${t(a(`createServer(specs, {
75
- stream: false, // disable streaming for all specs
76
- })`,"js"))}
77
- <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>
78
-
79
- ${r("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.")}
80
- `})};var s=document.getElementById("pulse-root");s&&!s.dataset.pulseMounted&&(s.dataset.pulseMounted="1",n(o,s,window.__PULSE_SERVER__||{},{ssr:!0}),m(s,n));var P=o;export{P as default};
@@ -1,164 +0,0 @@
1
- import{a as t}from"./runtime-QFURDKA2.js";import{a as c,b as a,c as u,d as p,e as r,g as e,h as d,i as o}from"./runtime-L2HNXIHW.js";import{a as n,b as h}from"./runtime-B73WLANC.js";var{prev:l,next:m}=c("/stripe"),i={route:"/stripe",meta:{title:"Payments (Stripe) \u2014 Pulse Docs",description:"Integrating Stripe Checkout and webhooks with Pulse using actions, raw response specs, and server data fetchers.",styles:["/docs.css"]},state:{},view:()=>a({currentHref:"/stripe",prev:l,next:m,content:`
2
- ${u("Payments (Stripe)")}
3
- ${p("Pulse uses Stripe's hosted Checkout \u2014 no client-side Stripe JS required. Checkout sessions are created server-side in an action's <code>run</code> function. Stripe handles the payment UI entirely. Webhooks are verified and handled through a raw response spec.")}
4
-
5
- ${o("info","Pulse has no external client-side JS. Use Stripe's hosted Checkout page (redirect flow) rather than Stripe Elements, which requires loading Stripe's client library.")}
6
-
7
- ${r("setup","Setup")}
8
- ${e(t("npm install stripe","bash"))}
9
- ${e(t(`# .env
10
- STRIPE_SECRET_KEY=sk_test_...
11
- STRIPE_WEBHOOK_SECRET=whsec_...
12
- APP_URL=http://localhost:3000`,"bash"))}
13
-
14
- ${r("checkout","Checkout action")}
15
- <p>Create a Stripe Checkout session in an action's <code>run</code> function and redirect the browser to it.</p>
16
- ${e(t(`// src/pages/pricing.js
17
- import Stripe from 'stripe'
18
-
19
- const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
20
- const APP_URL = process.env.APP_URL
21
-
22
- export default {
23
- route: '/pricing',
24
-
25
- state: { status: 'idle' },
26
-
27
- view: (state) => \`
28
- <main id="main-content">
29
- <h1>Pricing</h1>
30
- <form data-action="checkout">
31
- <input type="hidden" name="priceId" value="price_xxxx">
32
- <button type="submit">
33
- \${state.status === 'loading' ? 'Redirecting\u2026' : 'Buy now'}
34
- </button>
35
- </form>
36
- \${state.status === 'error'
37
- ? '<p role="alert">Something went wrong. Please try again.</p>'
38
- : ''}
39
- </main>
40
- \`,
41
-
42
- actions: {
43
- checkout: {
44
- onStart: () => ({ status: 'loading' }),
45
-
46
- run: async (state, serverState, formData) => {
47
- const priceId = formData.get('priceId')
48
-
49
- const session = await stripe.checkout.sessions.create({
50
- mode: 'payment',
51
- line_items: [{ price: priceId, quantity: 1 }],
52
- success_url: \`\${APP_URL}/checkout/success?session={CHECKOUT_SESSION_ID}\`,
53
- cancel_url: \`\${APP_URL}/checkout/cancel\`,
54
- })
55
-
56
- return { url: session.url }
57
- },
58
-
59
- onSuccess: (state, result) => {
60
- // Redirect to Stripe's hosted checkout page
61
- window.location.href = result.url
62
- return { status: 'redirecting' }
63
- },
64
-
65
- onError: () => ({ status: 'error' }),
66
- },
67
- },
68
- }`,"js"))}
69
-
70
- ${r("success","Success and cancel pages")}
71
- ${e(t(`// src/pages/checkout/success.js
72
- import Stripe from 'stripe'
73
- const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
74
-
75
- export default {
76
- route: '/checkout/success',
77
- meta: { title: 'Payment successful', styles: ['/app.css'] },
78
-
79
- server: {
80
- session: async (ctx) => {
81
- const { session } = ctx.query
82
- if (!session) return null
83
- return stripe.checkout.sessions.retrieve(session)
84
- },
85
- },
86
-
87
- state: {},
88
- view: (state, server) => \`
89
- <main id="main-content">
90
- <h1>Payment successful</h1>
91
- \${server.session
92
- ? \`<p>Thank you! Your order reference is <strong>\${server.session.id}</strong>.</p>\`
93
- : '<p>Thank you for your purchase.</p>'
94
- }
95
- <a href="/">Back to home</a>
96
- </main>
97
- \`,
98
- }`,"js"))}
99
-
100
- ${e(t(`// src/pages/checkout/cancel.js
101
- export default {
102
- route: '/checkout/cancel',
103
- meta: { title: 'Payment cancelled', styles: ['/app.css'] },
104
- state: {},
105
- view: () => \`
106
- <main id="main-content">
107
- <h1>Payment cancelled</h1>
108
- <p>No charge was made.</p>
109
- <a href="/pricing">Back to pricing</a>
110
- </main>
111
- \`,
112
- }`,"js"))}
113
-
114
- ${r("webhooks","Webhook handler")}
115
- <p>Stripe sends signed POST requests to your webhook endpoint. Use a raw response spec to verify the signature and handle events. The raw body is required for signature verification \u2014 access it via <code>ctx.rawBody</code> if your server is configured to populate it, or read it from the request stream.</p>
116
- ${e(t(`// src/pages/webhooks/stripe.js
117
- import Stripe from 'stripe'
118
-
119
- const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
120
- const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET
121
-
122
- export default {
123
- route: '/webhooks/stripe',
124
- contentType: 'application/json',
125
-
126
- server: {
127
- event: async (ctx) => {
128
- const sig = ctx.headers['stripe-signature']
129
- const body = ctx.rawBody // raw Buffer \u2014 see note below
130
-
131
- try {
132
- return stripe.webhooks.constructEvent(body, sig, webhookSecret)
133
- } catch (err) {
134
- return { error: err.message }
135
- }
136
- },
137
- },
138
-
139
- render: (ctx, server) => {
140
- if (server.event.error) {
141
- ctx.setHeader('X-Webhook-Error', server.event.error)
142
- return JSON.stringify({ error: server.event.error })
143
- }
144
-
145
- const event = server.event
146
-
147
- if (event.type === 'checkout.session.completed') {
148
- const session = event.data.object
149
- // fulfil the order...
150
- }
151
-
152
- if (event.type === 'customer.subscription.deleted') {
153
- // handle cancellation...
154
- }
155
-
156
- return JSON.stringify({ received: true })
157
- },
158
- }`,"js"))}
159
-
160
- ${o("warning","Stripe signature verification requires the raw request body before JSON parsing. Configure your Pulse server with <code>onRequest</code> to capture <code>ctx.rawBody</code> for the webhook route, or use a dedicated webhook path handled before Pulse's request pipeline.")}
161
-
162
- ${r("reference","Pattern summary")}
163
- ${d(["Concern","Pulse primitive"],[["Initiate checkout","<code>action.run</code> \u2014 calls Stripe API server-side, returns checkout URL"],["Redirect to Stripe","<code>action.onSuccess</code> \u2014 sets <code>window.location.href</code>"],["Confirm payment","<code>spec.server</code> on success page \u2014 retrieves session from Stripe"],["Handle webhooks","Raw response spec with <code>render</code> returning JSON"],["Verify signature","<code>spec.server</code> fetcher using <code>stripe.webhooks.constructEvent</code>"]])}
164
- `})};var s=document.getElementById("pulse-root");s&&!s.dataset.pulseMounted&&(s.dataset.pulseMounted="1",n(i,s,window.__PULSE_SERVER__||{},{ssr:!0}),h(s,n));var E=i;export{E as default};