@tanstack/router-core 1.171.14 → 1.171.16-pre.0

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 (139) hide show
  1. package/dist/cjs/Matches.cjs.map +1 -1
  2. package/dist/cjs/Matches.d.cts +3 -12
  3. package/dist/cjs/await-signal.cjs +21 -0
  4. package/dist/cjs/await-signal.cjs.map +1 -0
  5. package/dist/cjs/await-signal.d.cts +1 -0
  6. package/dist/cjs/index.cjs +3 -1
  7. package/dist/cjs/index.d.cts +3 -2
  8. package/dist/cjs/isServer/client.cjs +2 -0
  9. package/dist/cjs/isServer/client.cjs.map +1 -1
  10. package/dist/cjs/isServer/client.d.cts +1 -0
  11. package/dist/cjs/isServer/development.cjs +2 -0
  12. package/dist/cjs/isServer/development.cjs.map +1 -1
  13. package/dist/cjs/isServer/development.d.cts +1 -0
  14. package/dist/cjs/isServer/server.cjs +2 -0
  15. package/dist/cjs/isServer/server.cjs.map +1 -1
  16. package/dist/cjs/isServer/server.d.cts +1 -0
  17. package/dist/cjs/load-client.cjs +1470 -0
  18. package/dist/cjs/load-client.cjs.map +1 -0
  19. package/dist/cjs/load-client.d.cts +147 -0
  20. package/dist/cjs/load-server.cjs +565 -0
  21. package/dist/cjs/load-server.cjs.map +1 -0
  22. package/dist/cjs/load-server.d.cts +16 -0
  23. package/dist/cjs/redirect.cjs.map +1 -1
  24. package/dist/cjs/redirect.d.cts +0 -1
  25. package/dist/cjs/route.cjs.map +1 -1
  26. package/dist/cjs/router.cjs +187 -445
  27. package/dist/cjs/router.cjs.map +1 -1
  28. package/dist/cjs/router.d.cts +62 -52
  29. package/dist/cjs/scroll-restoration.cjs +27 -34
  30. package/dist/cjs/scroll-restoration.cjs.map +1 -1
  31. package/dist/cjs/ssr/client.cjs +2 -2
  32. package/dist/cjs/ssr/createRequestHandler.cjs +60 -9
  33. package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -1
  34. package/dist/cjs/ssr/createRequestHandler.d.cts +1 -0
  35. package/dist/cjs/ssr/handlerCallback.cjs +49 -4
  36. package/dist/cjs/ssr/handlerCallback.cjs.map +1 -1
  37. package/dist/cjs/ssr/handlerCallback.d.cts +3 -0
  38. package/dist/cjs/ssr/server.cjs +4 -0
  39. package/dist/cjs/ssr/server.d.cts +2 -2
  40. package/dist/cjs/ssr/ssr-client.d.cts +1 -10
  41. package/dist/cjs/ssr/ssr-match-id.cjs +2 -2
  42. package/dist/cjs/ssr/ssr-match-id.cjs.map +1 -1
  43. package/dist/cjs/ssr/ssr-server.cjs +6 -6
  44. package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
  45. package/dist/cjs/ssr/transformStreamWithRouter.cjs +27 -1
  46. package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -1
  47. package/dist/cjs/ssr/transformStreamWithRouter.d.cts +2 -0
  48. package/dist/cjs/ssr/types.d.cts +0 -1
  49. package/dist/cjs/stores.cjs +30 -104
  50. package/dist/cjs/stores.cjs.map +1 -1
  51. package/dist/cjs/stores.d.cts +7 -32
  52. package/dist/cjs/utils.cjs +0 -4
  53. package/dist/cjs/utils.cjs.map +1 -1
  54. package/dist/esm/Matches.d.ts +3 -12
  55. package/dist/esm/Matches.js.map +1 -1
  56. package/dist/esm/await-signal.d.ts +1 -0
  57. package/dist/esm/await-signal.js +21 -0
  58. package/dist/esm/await-signal.js.map +1 -0
  59. package/dist/esm/index.d.ts +3 -2
  60. package/dist/esm/index.js +3 -2
  61. package/dist/esm/isServer/client.d.ts +1 -0
  62. package/dist/esm/isServer/client.js +2 -1
  63. package/dist/esm/isServer/client.js.map +1 -1
  64. package/dist/esm/isServer/development.d.ts +1 -0
  65. package/dist/esm/isServer/development.js +2 -1
  66. package/dist/esm/isServer/development.js.map +1 -1
  67. package/dist/esm/isServer/server.d.ts +1 -0
  68. package/dist/esm/isServer/server.js +2 -1
  69. package/dist/esm/isServer/server.js.map +1 -1
  70. package/dist/esm/load-client.d.ts +147 -0
  71. package/dist/esm/load-client.js +1462 -0
  72. package/dist/esm/load-client.js.map +1 -0
  73. package/dist/esm/load-server.d.ts +16 -0
  74. package/dist/esm/load-server.js +565 -0
  75. package/dist/esm/load-server.js.map +1 -0
  76. package/dist/esm/redirect.d.ts +0 -1
  77. package/dist/esm/redirect.js.map +1 -1
  78. package/dist/esm/route.js.map +1 -1
  79. package/dist/esm/router.d.ts +62 -52
  80. package/dist/esm/router.js +188 -446
  81. package/dist/esm/router.js.map +1 -1
  82. package/dist/esm/scroll-restoration.js +27 -34
  83. package/dist/esm/scroll-restoration.js.map +1 -1
  84. package/dist/esm/ssr/client.js +1 -1
  85. package/dist/esm/ssr/createRequestHandler.d.ts +1 -0
  86. package/dist/esm/ssr/createRequestHandler.js +61 -11
  87. package/dist/esm/ssr/createRequestHandler.js.map +1 -1
  88. package/dist/esm/ssr/handlerCallback.d.ts +3 -0
  89. package/dist/esm/ssr/handlerCallback.js +47 -5
  90. package/dist/esm/ssr/handlerCallback.js.map +1 -1
  91. package/dist/esm/ssr/server.d.ts +2 -2
  92. package/dist/esm/ssr/server.js +3 -3
  93. package/dist/esm/ssr/ssr-client.d.ts +1 -10
  94. package/dist/esm/ssr/ssr-match-id.js +2 -2
  95. package/dist/esm/ssr/ssr-match-id.js.map +1 -1
  96. package/dist/esm/ssr/ssr-server.js +6 -6
  97. package/dist/esm/ssr/ssr-server.js.map +1 -1
  98. package/dist/esm/ssr/transformStreamWithRouter.d.ts +2 -0
  99. package/dist/esm/ssr/transformStreamWithRouter.js +27 -1
  100. package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -1
  101. package/dist/esm/ssr/types.d.ts +0 -1
  102. package/dist/esm/stores.d.ts +7 -32
  103. package/dist/esm/stores.js +30 -104
  104. package/dist/esm/stores.js.map +1 -1
  105. package/dist/esm/utils.js +1 -4
  106. package/dist/esm/utils.js.map +1 -1
  107. package/package.json +3 -3
  108. package/src/Matches.ts +6 -21
  109. package/src/await-signal.ts +27 -0
  110. package/src/index.ts +1 -3
  111. package/src/isServer/client.ts +1 -0
  112. package/src/isServer/development.ts +1 -0
  113. package/src/isServer/server.ts +1 -0
  114. package/src/load-client.ts +2701 -0
  115. package/src/load-server.ts +849 -0
  116. package/src/redirect.ts +0 -1
  117. package/src/route.ts +2 -11
  118. package/src/router.ts +380 -774
  119. package/src/scroll-restoration.ts +40 -56
  120. package/src/ssr/createRequestHandler.ts +109 -16
  121. package/src/ssr/handlerCallback.ts +76 -10
  122. package/src/ssr/server.ts +4 -1
  123. package/src/ssr/ssr-client.ts +1 -310
  124. package/src/ssr/ssr-match-id.ts +11 -2
  125. package/src/ssr/ssr-server.ts +9 -8
  126. package/src/ssr/transformStreamWithRouter.ts +37 -1
  127. package/src/ssr/types.ts +0 -1
  128. package/src/stores.ts +46 -208
  129. package/dist/cjs/load-matches.cjs +0 -658
  130. package/dist/cjs/load-matches.cjs.map +0 -1
  131. package/dist/cjs/load-matches.d.cts +0 -18
  132. package/dist/cjs/ssr/ssr-client.cjs +0 -183
  133. package/dist/cjs/ssr/ssr-client.cjs.map +0 -1
  134. package/dist/esm/load-matches.d.ts +0 -18
  135. package/dist/esm/load-matches.js +0 -656
  136. package/dist/esm/load-matches.js.map +0 -1
  137. package/dist/esm/ssr/ssr-client.js +0 -183
  138. package/dist/esm/ssr/ssr-client.js.map +0 -1
  139. package/src/load-matches.ts +0 -1278
@@ -1,5 +1,4 @@
1
1
  import { isServer } from '@tanstack/router-core/isServer'
2
- import { locationHistoryActions } from './router'
3
2
  import type { AnyRouter } from './router'
4
3
  import type { ParsedLocation } from './location'
5
4
 
@@ -133,7 +132,6 @@ export function getElementScrollRestorationEntry(
133
132
 
134
133
  let ignoreScroll = false
135
134
  const windowScrollTarget = 'window'
136
- type ScrollTarget = typeof windowScrollTarget | Element
137
135
 
138
136
  function getElement(selector: string | (() => Element | null | undefined)) {
139
137
  try {
@@ -148,8 +146,8 @@ function getScrollToTopElements(
148
146
  scrollToTopSelectors: NonNullable<
149
147
  AnyRouter['options']['scrollToTopSelectors']
150
148
  >,
151
- ): Array<Element> {
152
- const elements: Array<Element> = []
149
+ ) {
150
+ const elements = new Set<Element>()
153
151
 
154
152
  for (const selector of scrollToTopSelectors) {
155
153
  if (selector === windowScrollTarget) {
@@ -158,7 +156,7 @@ function getScrollToTopElements(
158
156
 
159
157
  const element = getElement(selector)
160
158
  if (element) {
161
- elements.push(element)
159
+ elements.add(element)
162
160
  }
163
161
  }
164
162
 
@@ -180,46 +178,21 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
180
178
 
181
179
  const getKey =
182
180
  router.options.getScrollRestorationKey || defaultGetScrollRestorationKey
183
- const trackedScrollEntries = new Map<ScrollTarget, ScrollRestorationEntry>()
184
- const setTrackedScrollEntry = (
185
- target: ScrollTarget,
186
- scrollX: number,
187
- scrollY: number,
188
- ) => {
189
- const entry =
190
- trackedScrollEntries.get(target) || ({} as ScrollRestorationEntry)
191
- entry.scrollX = scrollX
192
- entry.scrollY = scrollY
193
- trackedScrollEntries.set(target, entry)
194
- }
195
-
196
- const onScroll = (event: Event) => {
197
- if (ignoreScroll || !scroll.restoring) {
198
- return
199
- }
200
-
201
- if (event.target === document) {
202
- setTrackedScrollEntry(windowScrollTarget, scrollX, scrollY)
203
- } else {
204
- const target = event.target as Element
205
- setTrackedScrollEntry(target, target.scrollLeft, target.scrollTop)
206
- }
207
- }
181
+ const trackedScrollTargets = new Set<Document | Element>()
208
182
 
209
183
  // Snapshot the current page's tracked scroll targets before navigation or unload.
210
184
  const snapshotCurrentScrollTargets = (restoreKey: string) => {
211
- if (!scroll.restoring) {
212
- return
213
- }
214
-
215
185
  const keyEntry = (scrollRestorationCache[restoreKey] ||=
216
186
  {} as ScrollRestorationByElement)
217
187
 
218
- for (const [target, position] of trackedScrollEntries) {
219
- if (target === windowScrollTarget) {
220
- keyEntry[windowScrollTarget] = position
221
- } else if (target.isConnected) {
222
- keyEntry[getScrollRestorationSelector(target)] = position
188
+ for (const target of trackedScrollTargets) {
189
+ if (target === document) {
190
+ keyEntry[windowScrollTarget] = { scrollX, scrollY }
191
+ } else if ((target as Element).isConnected) {
192
+ keyEntry[getScrollRestorationSelector(target as Element)] = {
193
+ scrollX: (target as Element).scrollLeft,
194
+ scrollY: (target as Element).scrollTop,
195
+ }
223
196
  }
224
197
  }
225
198
  }
@@ -230,12 +203,21 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
230
203
 
231
204
  history.scrollRestoration = 'manual'
232
205
 
233
- document.addEventListener('scroll', onScroll, true)
206
+ document.addEventListener(
207
+ 'scroll',
208
+ (event) => {
209
+ if (ignoreScroll) {
210
+ return
211
+ }
212
+ trackedScrollTargets.add(event.target as Document | Element)
213
+ },
214
+ true,
215
+ )
234
216
  router.subscribe('onBeforeLoad', (event) => {
235
217
  if (event.fromLocation) {
236
218
  snapshotCurrentScrollTargets(getKey(event.fromLocation))
237
219
  }
238
- trackedScrollEntries.clear()
220
+ trackedScrollTargets.clear()
239
221
  })
240
222
  addEventListener('pagehide', () => {
241
223
  snapshotCurrentScrollTargets(
@@ -258,12 +240,11 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
258
240
  const behavior = router.options.scrollRestorationBehavior
259
241
  const scrollToTopSelectors = router.options.scrollToTopSelectors
260
242
  const shouldResetScroll = scroll.next
261
- let scrollToTopElements: Array<Element> | undefined
262
- trackedScrollEntries.clear()
263
-
264
- if (!shouldResetScroll) {
265
- scroll.next = true
266
- }
243
+ const hashNavigation = scroll.hash
244
+ let scrollToTopElements: Set<Element> | undefined
245
+ trackedScrollTargets.clear()
246
+ scroll.next = true
247
+ scroll.hash = false
267
248
 
268
249
  if (
269
250
  typeof router.options.scrollRestoration === 'function' &&
@@ -295,7 +276,7 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
295
276
  if (shouldResetScroll && scrollToTopSelectors) {
296
277
  scrollToTopElements ??=
297
278
  getScrollToTopElements(scrollToTopSelectors)
298
- if (scrollToTopElements.includes(element)) {
279
+ if (scrollToTopElements.has(element)) {
299
280
  continue
300
281
  }
301
282
  }
@@ -321,11 +302,12 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
321
302
  let windowRestored = false
322
303
 
323
304
  if (shouldResetScroll) {
324
- const action = locationHistoryActions.get(event.toLocation)
305
+ if (!hash && scrollToTopSelectors) {
306
+ scrollToTopElements ??= getScrollToTopElements(scrollToTopSelectors)
307
+ }
308
+
325
309
  const skipWindowRestore =
326
- hash &&
327
- hashScrollIntoViewOptions &&
328
- (action === 'PUSH' || action === 'REPLACE')
310
+ hash && hashScrollIntoViewOptions && hashNavigation
329
311
 
330
312
  const elementEntries = scroll.restoring
331
313
  ? scrollRestorationCache[cacheKey]
@@ -351,21 +333,23 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
351
333
  if (element) {
352
334
  element.scrollLeft = scrollX
353
335
  element.scrollTop = scrollY
336
+ scrollToTopElements?.delete(element)
354
337
  }
355
338
  }
356
339
  }
357
340
  }
358
341
 
359
- if (!windowRestored && !hash) {
342
+ if (!hash) {
360
343
  const scrollOptions = {
361
344
  top: 0,
362
345
  left: 0,
363
346
  behavior,
364
347
  }
365
348
 
366
- scrollTo(scrollOptions)
367
- if (scrollToTopSelectors) {
368
- scrollToTopElements ??= getScrollToTopElements(scrollToTopSelectors)
349
+ if (!windowRestored) {
350
+ scrollTo(scrollOptions)
351
+ }
352
+ if (scrollToTopElements) {
369
353
  for (const element of scrollToTopElements) {
370
354
  element.scrollTo(scrollOptions)
371
355
  }
@@ -1,11 +1,15 @@
1
1
  import { createMemoryHistory } from '@tanstack/history'
2
+ import { _getRenderedMatches } from '../load-client'
2
3
  import { mergeHeaders } from './headers'
3
4
  import {
4
5
  attachRouterServerSsrUtils,
5
6
  getNormalizedURL,
6
7
  getOrigin,
7
8
  } from './ssr-server'
8
- import { normalizeSsrResponse } from './handlerCallback'
9
+ import {
10
+ bindSsrResponseToRequest,
11
+ disposeSsrResponseDetached,
12
+ } from './handlerCallback'
9
13
  import type { HandlerCallback } from './handlerCallback'
10
14
  import type { AnyHeaders } from './headers'
11
15
  import type { AnyRouter } from '../router'
@@ -15,6 +19,79 @@ export type RequestHandler<TRouter extends AnyRouter> = (
15
19
  cb: HandlerCallback<TRouter>,
16
20
  ) => Promise<Response>
17
21
 
22
+ type RequestWaiter = ((reason: unknown) => void) | undefined
23
+
24
+ const requestWaiters = new WeakMap<AbortSignal, Array<RequestWaiter>>()
25
+
26
+ function removeRequestWaiter(
27
+ waiters: Array<RequestWaiter>,
28
+ index: number,
29
+ reject: (reason: unknown) => void,
30
+ ) {
31
+ if (waiters[index] !== reject) {
32
+ return
33
+ }
34
+ if (index !== waiters.length - 1) {
35
+ waiters[index] = undefined
36
+ return
37
+ }
38
+
39
+ waiters.pop()
40
+ while (waiters.length && waiters[waiters.length - 1] === undefined) {
41
+ waiters.pop()
42
+ }
43
+ }
44
+
45
+ export function waitForRequest<T>(
46
+ value: T | PromiseLike<T>,
47
+ signal: AbortSignal,
48
+ onLate?: (value: T) => void,
49
+ ): Promise<T> {
50
+ const promise = Promise.resolve(value)
51
+ if (signal.aborted) {
52
+ void promise.then(onLate, () => {})
53
+ return Promise.reject(signal.reason)
54
+ }
55
+
56
+ return new Promise<T>((resolve, reject) => {
57
+ let waiters = requestWaiters.get(signal)
58
+ let index: number
59
+ if (waiters) {
60
+ index = waiters.push(reject) - 1
61
+ } else {
62
+ const newWaiters: Array<RequestWaiter> = [reject]
63
+ waiters = newWaiters
64
+ index = 0
65
+ requestWaiters.set(signal, newWaiters)
66
+ signal.addEventListener(
67
+ 'abort',
68
+ () => {
69
+ requestWaiters.delete(signal)
70
+ for (const rejectWaiter of newWaiters) {
71
+ rejectWaiter?.(signal.reason)
72
+ }
73
+ newWaiters.length = 0
74
+ },
75
+ { once: true },
76
+ )
77
+ }
78
+ void promise.then(
79
+ (result) => {
80
+ removeRequestWaiter(waiters, index, reject)
81
+ if (signal.aborted) {
82
+ onLate?.(result)
83
+ } else {
84
+ resolve(result)
85
+ }
86
+ },
87
+ (error) => {
88
+ removeRequestWaiter(waiters, index, reject)
89
+ reject(error)
90
+ },
91
+ )
92
+ })
93
+ }
94
+
18
95
  export function createRequestHandler<TRouter extends AnyRouter>({
19
96
  createRouter,
20
97
  request,
@@ -25,13 +102,14 @@ export function createRequestHandler<TRouter extends AnyRouter>({
25
102
  getRouterManifest?: () => ServerManifest | Promise<ServerManifest>
26
103
  }): RequestHandler<TRouter> {
27
104
  return async (cb) => {
105
+ request.signal.throwIfAborted()
28
106
  const router = createRouter()
29
107
  let responseOwnsCleanup = false
30
108
 
31
109
  try {
32
110
  attachRouterServerSsrUtils({
33
111
  router,
34
- manifest: await getRouterManifest?.(),
112
+ manifest: await waitForRequest(getRouterManifest?.(), request.signal),
35
113
  })
36
114
 
37
115
  // normalizing and sanitizing the pathname here for server, so we always deal with the same format during SSR.
@@ -50,20 +128,41 @@ export function createRequestHandler<TRouter extends AnyRouter>({
50
128
  origin: router.options.origin ?? origin,
51
129
  })
52
130
 
53
- await router.load()
131
+ await router.load({
132
+ _signal: request.signal,
133
+ })
134
+ request.signal.throwIfAborted()
135
+
136
+ const result = router._serverResult
137
+ if (result?.type === 'redirect') {
138
+ return result.redirect
139
+ }
54
140
 
55
- await router.serverSsr?.dehydrate()
141
+ await waitForRequest(router.serverSsr?.dehydrate(), request.signal)
142
+ request.signal.throwIfAborted()
56
143
 
57
144
  const responseHeaders = getRequestHeaders({
58
145
  router,
59
146
  })
60
147
 
61
- const response = await cb({
62
- request,
148
+ request.signal.throwIfAborted()
149
+ const response = await waitForRequest(
150
+ cb({
151
+ request,
152
+ router,
153
+ responseHeaders,
154
+ }),
155
+ request.signal,
156
+ (late) => {
157
+ disposeSsrResponseDetached(late, request.signal.reason)
158
+ },
159
+ )
160
+ const ssrResponse = bindSsrResponseToRequest(
63
161
  router,
64
- responseHeaders,
65
- })
66
- const ssrResponse = normalizeSsrResponse(response)
162
+ response,
163
+ request.signal,
164
+ )
165
+ request.signal.throwIfAborted()
67
166
  responseOwnsCleanup = ssrResponse.serverSsrCleanup === 'stream'
68
167
  return ssrResponse.response
69
168
  } finally {
@@ -79,16 +178,10 @@ export function createRequestHandler<TRouter extends AnyRouter>({
79
178
 
80
179
  function getRequestHeaders(opts: { router: AnyRouter }): Headers {
81
180
  const matchHeaders: Array<AnyHeaders> = []
82
- for (const match of opts.router.stores.matches.get()) {
181
+ for (const match of _getRenderedMatches(opts.router.stores.matches.get())) {
83
182
  matchHeaders.push(match.headers)
84
183
  }
85
184
 
86
- // Handle Redirects
87
- const redirect = opts.router.stores.redirect.get()
88
- if (redirect) {
89
- matchHeaders.push(redirect.headers)
90
- }
91
-
92
185
  return mergeHeaders(
93
186
  {
94
187
  'Content-Type': 'text/html; charset=UTF-8',
@@ -30,6 +30,40 @@ export function normalizeSsrResponse(
30
30
  : { response: result, serverSsrCleanup: 'none' }
31
31
  }
32
32
 
33
+ export function disposeSsrResponse(
34
+ response: SsrResponse,
35
+ reason?: unknown,
36
+ ): Promise<void> {
37
+ if (response.serverSsrCleanup !== 'stream') {
38
+ return Promise.resolve()
39
+ }
40
+ try {
41
+ return Promise.resolve(response.dispose(reason))
42
+ } catch (error) {
43
+ return Promise.reject(error)
44
+ }
45
+ }
46
+
47
+ export function disposeSsrResponseDetached(
48
+ result: HandlerCallbackResult,
49
+ reason?: unknown,
50
+ onError: (error: unknown) => void = console.error,
51
+ ): void {
52
+ const ssrResponse = normalizeSsrResponse(result)
53
+ if (ssrResponse.serverSsrCleanup === 'stream') {
54
+ void disposeSsrResponse(ssrResponse, reason).catch(onError)
55
+ return
56
+ }
57
+
58
+ if (ssrResponse.response.body) {
59
+ try {
60
+ void ssrResponse.response.body.cancel(reason).catch(onError)
61
+ } catch (error) {
62
+ onError(error)
63
+ }
64
+ }
65
+ }
66
+
33
67
  export function createSsrStreamResponse<TRouter extends AnyRouter>(
34
68
  router: TRouter,
35
69
  response: Response,
@@ -43,29 +77,63 @@ export function createSsrStreamResponse<TRouter extends AnyRouter>(
43
77
  response,
44
78
  serverSsrCleanup: 'stream',
45
79
  async dispose(reason?: unknown) {
46
- if (disposed) return
80
+ if (disposed) {
81
+ return
82
+ }
47
83
  disposed = true
48
84
 
85
+ // Sever router ownership before asking user/renderer stream machinery to
86
+ // cancel. A custom stream is allowed to ignore cancellation forever.
87
+ router.serverSsr?.cleanup()
88
+
49
89
  try {
50
90
  await response.body!.cancel(reason)
51
91
  } catch {
52
- // ignore; fallback cleanup below still releases router SSR state
92
+ // Cleanup above already released router SSR state.
53
93
  }
54
-
55
- router.serverSsr?.cleanup()
56
94
  },
57
95
  }
58
96
  }
59
97
 
98
+ export function bindSsrResponseToRequest(
99
+ router: AnyRouter | undefined,
100
+ result: HandlerCallbackResult,
101
+ signal: AbortSignal,
102
+ ): SsrResponse {
103
+ const ssrResponse = normalizeSsrResponse(result)
104
+ if (ssrResponse.serverSsrCleanup !== 'stream') {
105
+ if (signal.aborted) {
106
+ disposeSsrResponseDetached(result, signal.reason)
107
+ }
108
+ return ssrResponse
109
+ }
110
+
111
+ const failed = (error: unknown) => {
112
+ router?.serverSsr?.cleanup()
113
+ console.error(error)
114
+ }
115
+ const abort = () => {
116
+ disposeSsrResponseDetached(ssrResponse, signal.reason, failed)
117
+ }
118
+ if (signal.aborted) {
119
+ abort()
120
+ return ssrResponse
121
+ }
122
+
123
+ signal.addEventListener('abort', abort, { once: true })
124
+ router?.serverSsr?.onCleanup(() => {
125
+ signal.removeEventListener('abort', abort)
126
+ })
127
+ return ssrResponse
128
+ }
129
+
60
130
  export async function replaceSsrResponse(
61
131
  result: HandlerCallbackResult,
62
132
  response: Response,
63
133
  reason?: unknown,
64
134
  ): Promise<SsrResponse> {
65
135
  const ssrResponse = normalizeSsrResponse(result)
66
- if (ssrResponse.serverSsrCleanup === 'stream') {
67
- await ssrResponse.dispose(reason)
68
- }
136
+ await disposeSsrResponse(ssrResponse, reason)
69
137
  return { response, serverSsrCleanup: 'none' }
70
138
  }
71
139
 
@@ -74,9 +142,7 @@ export async function stripSsrResponseBody(
74
142
  reason?: unknown,
75
143
  ): Promise<SsrResponse> {
76
144
  const ssrResponse = normalizeSsrResponse(result)
77
- if (ssrResponse.serverSsrCleanup === 'stream') {
78
- await ssrResponse.dispose(reason)
79
- }
145
+ await disposeSsrResponse(ssrResponse, reason)
80
146
  return {
81
147
  response: new Response(null, ssrResponse.response),
82
148
  serverSsrCleanup: 'none',
package/src/ssr/server.ts CHANGED
@@ -1,8 +1,11 @@
1
- export { createRequestHandler } from './createRequestHandler'
1
+ export { createRequestHandler, waitForRequest } from './createRequestHandler'
2
2
  export type { RequestHandler } from './createRequestHandler'
3
3
  export {
4
+ bindSsrResponseToRequest,
4
5
  createSsrStreamResponse,
5
6
  defineHandlerCallback,
7
+ disposeSsrResponse,
8
+ disposeSsrResponseDetached,
6
9
  isSsrResponse,
7
10
  normalizeSsrResponse,
8
11
  replaceSsrResponse,