@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 +6 -0
- package/package.json +1 -1
- package/src/lib/browser/page.svelte.ts +33 -9
- package/src/lib/browser/startClient.ts +11 -1
- package/src/lib/server/runtime/createServer.ts +3 -0
- package/src/lib/server/runtime/types/RequestStore.ts +7 -0
- package/src/lib/shared/activePage.ts +20 -0
- package/src/lib/shared/pageSlot.ts +17 -0
- package/src/lib/shared/setPageResolver.ts +7 -0
- package/src/lib/shared/types/PageSnapshot.ts +11 -0
- package/src/serverEntry.ts +16 -0
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,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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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 ===
|
|
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 ===
|
|
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 {
|
|
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
|
+
}
|
package/src/serverEntry.ts
CHANGED
|
@@ -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.
|