@belte/belte 0.19.0 → 0.19.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @belte/belte
2
2
 
3
+ ## 0.19.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`c392c5a`](https://github.com/briancray/belte/commit/c392c5abf08a070617e7bfa1094cc38a20cba002) - read the page store during SSR via a request-scoped resolver ([`85f4c7a`](https://github.com/briancray/belte/commit/85f4c7a486191847772553cd62b34315a15a6438))
8
+
3
9
  ## 0.18.0
4
10
 
5
11
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@belte/belte",
3
- "version": "0.19.0",
3
+ "version": "0.19.1",
4
4
  "type": "module",
5
5
  "description": "Isomorphic multimodal HTTP framework built for humans and machines in a single Bun runtime",
6
6
  "license": "MIT",
@@ -1,9 +1,11 @@
1
1
  import type { Component } from 'svelte'
2
+ import { activePage } from '../shared/activePage.ts'
2
3
  import {
3
4
  type NormalizedLayoutPrefix,
4
5
  nearestLayoutPrefix,
5
6
  normalizeLayoutPrefixes,
6
7
  } from '../shared/nearestLayoutPrefix.ts'
8
+ import type { PageSnapshot } from '../shared/types/PageSnapshot.ts'
7
9
  import { abortPageStream } from './pageStreamController.ts'
8
10
  import type { Layouts } from './types/Layouts.ts'
9
11
  import type { Pages } from './types/Pages.ts'
@@ -42,13 +44,37 @@ export type Page = keyof Routes extends never
42
44
  ? PageStateFor<string>
43
45
  : { [R in keyof Routes]: PageStateFor<R> }[keyof Routes]
44
46
 
45
- // biome-ignore lint/suspicious/noExplicitAny: discriminated-union init needs a single arm
46
- export const page: Page = $state<any>({
47
+ /*
48
+ Client-side singleton the resolver returns. navigate()/applyState mutate it;
49
+ because it's $state, reads taken through the `page` proxy inside a $derived
50
+ re-run on every nav. Never populated server-side — there the resolver reads
51
+ the per-request ALS store instead, so concurrent renders don't share it.
52
+ */
53
+ export const clientPageState: PageSnapshot = $state({
47
54
  route: '',
48
55
  params: {},
49
56
  url: new URL('http://localhost/'),
50
57
  })
51
58
 
59
+ /*
60
+ Public page state. A getter proxy over the side's registered resolver
61
+ (activePage): client → clientPageState singleton, server → the per-request
62
+ ALS snapshot. Property reads on the client resolve to the $state singleton, so
63
+ reactivity flows; on the server each render reads its own request scope. Users
64
+ only ever see route/params/url.
65
+ */
66
+ export const page = {
67
+ get route() {
68
+ return activePage().route
69
+ },
70
+ get params() {
71
+ return activePage().params
72
+ },
73
+ get url() {
74
+ return activePage().url
75
+ },
76
+ } as unknown as Page
77
+
52
78
  /*
53
79
  Internal renderer state — the Layout/Page components App.svelte mounts.
54
80
  Kept on a separate $state object so it doesn't leak into the public `page`
@@ -116,15 +142,13 @@ function applyState(
116
142
  ): void {
117
143
  renderState.Layout = Layout
118
144
  renderState.Page = Page
119
- const mutable = page as PageStateFor<string>
120
- mutable.route = route
121
- mutable.params = params
145
+ clientPageState.route = route
146
+ clientPageState.params = params
122
147
  syncUrl()
123
148
  }
124
149
 
125
150
  function syncUrl(): void {
126
- const mutable = page as PageStateFor<string>
127
- mutable.url = new URL(window.location.href)
151
+ clientPageState.url = new URL(window.location.href)
128
152
  }
129
153
 
130
154
  /*
@@ -207,7 +231,7 @@ export async function navigate(href: string, options: NavigateOptions = {}): Pro
207
231
  return
208
232
  }
209
233
  const fullTarget = `${target.pathname}${target.search}${target.hash}`
210
- if (target.pathname === page.url.pathname) {
234
+ if (target.pathname === clientPageState.url.pathname) {
211
235
  writeHistory(replace, fullTarget)
212
236
  syncUrl()
213
237
  return
@@ -234,7 +258,7 @@ only refreshes `page.url`; a pathname change resolves and swaps the page, or
234
258
  hard-navigates when the restored URL isn't an SPA route.
235
259
  */
236
260
  async function applyTarget(pathname: string, fullTarget: string): Promise<void> {
237
- if (pathname === page.url.pathname) {
261
+ if (pathname === clientPageState.url.pathname) {
238
262
  syncUrl()
239
263
  return
240
264
  }
@@ -3,13 +3,21 @@ import App from '../../App.svelte'
3
3
  import { createCacheStore } from '../shared/createCacheStore.ts'
4
4
  import { setCacheStoreResolver } from '../shared/setCacheStoreResolver.ts'
5
5
  import { setGlobalCacheStoreResolver } from '../shared/setGlobalCacheStoreResolver.ts'
6
+ import { setPageResolver } from '../shared/setPageResolver.ts'
6
7
  import type { CacheSnapshotEntry } from '../shared/types/CacheSnapshotEntry.ts'
7
8
  import type { CacheStore } from '../shared/types/CacheStore.ts'
8
9
  import type { StreamingPlaceholder } from '../shared/types/StreamingPlaceholder.ts'
9
10
  import { cacheEntryFromSnapshot } from './cacheEntryFromSnapshot.ts'
10
11
  import { installStreamingPlaceholders } from './installStreamingPlaceholders.ts'
11
12
  import { openResolveStream } from './openResolveStream.ts'
12
- import { bindPage, handlePopstate, navigate, page, renderState } from './page.svelte.ts'
13
+ import {
14
+ bindPage,
15
+ clientPageState,
16
+ handlePopstate,
17
+ navigate,
18
+ page,
19
+ renderState,
20
+ } from './page.svelte.ts'
13
21
  import type { Layouts } from './types/Layouts.ts'
14
22
  import type { Pages } from './types/Pages.ts'
15
23
 
@@ -107,6 +115,8 @@ export async function startClient({
107
115
  setCacheStoreResolver(() => cacheStore)
108
116
  /* One tab store: cache(fn, { global: true }) shares it, so global is a no-op here. */
109
117
  setGlobalCacheStoreResolver(() => cacheStore)
118
+ /* One document: the `page` proxy resolves to this $state singleton, mutated by navigate(). */
119
+ setPageResolver(() => clientPageState)
110
120
  if (window.__SSR__.cache) {
111
121
  hydrateCacheFromSnapshot(cacheStore, window.__SSR__.cache)
112
122
  }
@@ -293,6 +293,9 @@ export async function createServer({
293
293
  params: Record<string, string>,
294
294
  store: RequestStore,
295
295
  ): Promise<Response> {
296
+ /* Publish the match so the `page` proxy resolves route/params during SSR render. */
297
+ store.route = routeUrl
298
+ store.params = params
296
299
  const json = wantsJson(store.req)
297
300
  if (json) {
298
301
  return Response.json(
@@ -12,6 +12,13 @@ export type RequestStore = {
12
12
  req: Request
13
13
  cache: CacheStore
14
14
  /*
15
+ The matched page route and its decoded params, set just before the page
16
+ renders so the `page` proxy resolves them inside layout-scoped components
17
+ during SSR. Undefined on rpc/socket requests and until a page match lands.
18
+ */
19
+ route?: string
20
+ params?: Record<string, string>
21
+ /*
15
22
  The request's cookie jar, materialized lazily by the first cookies() call
16
23
  and flushed to Set-Cookie headers when the scope returns. Undefined while a
17
24
  request never touches cookies, so the common path parses and emits nothing.
@@ -0,0 +1,20 @@
1
+ import { pageSlot } from './pageSlot.ts'
2
+ import type { PageSnapshot } from './types/PageSnapshot.ts'
3
+
4
+ /*
5
+ Resolves the active page snapshot. The runtime is registered via
6
+ `setPageResolver` from the server entry (request-scoped via ALS) or the
7
+ client entry (module-level singleton). If no resolver is registered, a single
8
+ fallback snapshot is created lazily so isolated tests still work. Mirrors
9
+ activeCacheStore.
10
+ */
11
+ export function activePage(): PageSnapshot {
12
+ const fromResolver = pageSlot.resolver?.()
13
+ if (fromResolver) {
14
+ return fromResolver
15
+ }
16
+ if (!pageSlot.fallback) {
17
+ pageSlot.fallback = { route: '', params: {}, url: new URL('http://localhost/') }
18
+ }
19
+ return pageSlot.fallback
20
+ }
@@ -0,0 +1,17 @@
1
+ import type { PageSnapshot } from './types/PageSnapshot.ts'
2
+
3
+ /*
4
+ Internal slot the runtime entries register their page resolver into. The
5
+ server entry installs an ALS-backed resolver (request-scoped, so concurrent
6
+ and streaming renders never share state); the client entry installs a
7
+ module-singleton resolver. `fallback` is a single lazy snapshot used only
8
+ when no resolver is registered — keeps isolated tests working without forcing
9
+ them to spin up the runtime. Mirrors cacheStoreSlot.
10
+ */
11
+ export const pageSlot: {
12
+ resolver: (() => PageSnapshot | undefined) | undefined
13
+ fallback: PageSnapshot | undefined
14
+ } = {
15
+ resolver: undefined,
16
+ fallback: undefined,
17
+ }
@@ -0,0 +1,7 @@
1
+ import { pageSlot } from './pageSlot.ts'
2
+ import type { PageSnapshot } from './types/PageSnapshot.ts'
3
+
4
+ // Registers the runtime's page resolver. Called once per side at boot.
5
+ export function setPageResolver(fn: () => PageSnapshot | undefined): void {
6
+ pageSlot.resolver = fn
7
+ }
@@ -0,0 +1,11 @@
1
+ /*
2
+ Side-agnostic shape of the active page state: the matched route, its decoded
3
+ params, and the live request/location URL. The browser `page` proxy reads its
4
+ public fields through this; the server resolver reads them off the per-request
5
+ store, the client resolver off the module singleton.
6
+ */
7
+ export type PageSnapshot = {
8
+ route: string
9
+ params: Record<string, string>
10
+ url: URL
11
+ }
@@ -35,6 +35,7 @@ import { loadEnvFromDataDir } from './lib/shared/loadEnvFromDataDir.ts'
35
35
  import { runningAsStandaloneBinary } from './lib/shared/runningAsStandaloneBinary.ts'
36
36
  import { setCacheStoreResolver } from './lib/shared/setCacheStoreResolver.ts'
37
37
  import { setGlobalCacheStoreResolver } from './lib/shared/setGlobalCacheStoreResolver.ts'
38
+ import { setPageResolver } from './lib/shared/setPageResolver.ts'
38
39
 
39
40
  /*
40
41
  Resolve config into process.env before anything reads it (createServer reads
@@ -67,6 +68,21 @@ exitWithParent()
67
68
 
68
69
  setCacheStoreResolver(() => requestContext.getStore()?.cache)
69
70
 
71
+ /*
72
+ Request-scoped page resolver: the `page` proxy reads route/params/url off the
73
+ ALS store, so layout-scoped components see the live match during SSR without a
74
+ module singleton leaking across concurrent or streaming renders. route/params
75
+ land just before render; url is set at the request boundary, so 404/error
76
+ renders still get a correct page.url.
77
+ */
78
+ setPageResolver(() => {
79
+ const store = requestContext.getStore()
80
+ if (!store) {
81
+ return undefined
82
+ }
83
+ return { route: store.route ?? '', params: store.params ?? {}, url: store.url }
84
+ })
85
+
70
86
  /*
71
87
  Process-level store for cache(fn, { global: true }) — one per server process,
72
88
  outlives every request so memoised external calls are shared across them.