@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,310 +1 @@
1
- import { invariant } from '../invariant'
2
- import { isNotFound } from '../not-found'
3
- import { createControlledPromise } from '../utils'
4
- import { hydrateSsrMatchId } from './ssr-match-id'
5
- import type { GLOBAL_SEROVAL, GLOBAL_TSR } from './constants'
6
- import type { DehydratedMatch, TsrSsrGlobal } from './types'
7
- import type { AnyRouteMatch } from '../Matches'
8
- import type { AnyRouter } from '../router'
9
- import type { RouteContextOptions } from '../route'
10
- import type { AnySerializationAdapter } from './serializer/transformer'
11
-
12
- declare global {
13
- interface Window {
14
- [GLOBAL_TSR]?: TsrSsrGlobal
15
- [GLOBAL_SEROVAL]?: any
16
- }
17
- }
18
-
19
- function hydrateMatch(
20
- match: AnyRouteMatch,
21
- deyhydratedMatch: DehydratedMatch,
22
- ): void {
23
- match.id = deyhydratedMatch.i
24
- match.__beforeLoadContext = deyhydratedMatch.b
25
- match.loaderData = deyhydratedMatch.l
26
- match.status = deyhydratedMatch.s
27
- match.ssr = deyhydratedMatch.ssr
28
- match.updatedAt = deyhydratedMatch.u
29
- match.error = deyhydratedMatch.e
30
- // Only hydrate global-not-found when a defined value is present in the
31
- // dehydrated payload. If omitted, preserve the value computed from the
32
- // current client location (important for SPA fallback HTML served at unknown
33
- // URLs, where dehydrated matches may come from `/` but client matching marks
34
- // root as globalNotFound).
35
- if (deyhydratedMatch.g !== undefined) {
36
- match.globalNotFound = deyhydratedMatch.g
37
- }
38
- }
39
-
40
- export async function hydrate(router: AnyRouter): Promise<any> {
41
- if (!window.$_TSR) {
42
- if (process.env.NODE_ENV !== 'production') {
43
- throw new Error(
44
- 'Invariant failed: Expected to find bootstrap data on window.$_TSR, but we did not. Please file an issue!',
45
- )
46
- }
47
-
48
- invariant()
49
- }
50
-
51
- const serializationAdapters = router.options.serializationAdapters as
52
- | Array<AnySerializationAdapter>
53
- | undefined
54
-
55
- if (serializationAdapters?.length) {
56
- const fromSerializableMap = new Map()
57
- serializationAdapters.forEach((adapter) => {
58
- fromSerializableMap.set(adapter.key, adapter.fromSerializable)
59
- })
60
- window.$_TSR.t = fromSerializableMap
61
- window.$_TSR.buffer.forEach((script) => script())
62
- }
63
- window.$_TSR.initialized = true
64
-
65
- if (!window.$_TSR.router) {
66
- if (process.env.NODE_ENV !== 'production') {
67
- throw new Error(
68
- 'Invariant failed: Expected to find a dehydrated data on window.$_TSR.router, but we did not. Please file an issue!',
69
- )
70
- }
71
-
72
- invariant()
73
- }
74
-
75
- const dehydratedRouter = window.$_TSR.router
76
- dehydratedRouter.matches.forEach((dehydratedMatch) => {
77
- dehydratedMatch.i = hydrateSsrMatchId(dehydratedMatch.i)
78
- })
79
- if (dehydratedRouter.lastMatchId) {
80
- dehydratedRouter.lastMatchId = hydrateSsrMatchId(
81
- dehydratedRouter.lastMatchId,
82
- )
83
- }
84
- const { manifest, dehydratedData, lastMatchId } = dehydratedRouter
85
-
86
- router.ssr = {
87
- manifest,
88
- }
89
- const meta = document.querySelector('meta[property="csp-nonce"]') as
90
- | HTMLMetaElement
91
- | undefined
92
- const nonce = meta?.content
93
- router.options.ssr = {
94
- nonce,
95
- }
96
-
97
- // Allow the user to handle custom hydration data before matching routes.
98
- // This lets hydration install router config that affects matching, e.g. rewrites.
99
- await router.options.hydrate?.(dehydratedData)
100
-
101
- // Hydrate the router state
102
- const matches = router.matchRoutes(router.stores.location.get())
103
-
104
- // kick off loading the route chunks
105
- const routeChunkPromise = Promise.all(
106
- matches.map((match) =>
107
- router.loadRouteChunk(router.looseRoutesById[match.routeId]!),
108
- ),
109
- )
110
-
111
- function setMatchForcePending(match: AnyRouteMatch) {
112
- // usually the minPendingPromise is created in the Match component if a pending match is rendered
113
- // however, this might be too late if the match synchronously resolves
114
- const route = router.looseRoutesById[match.routeId]!
115
- const pendingMinMs =
116
- route.options.pendingMinMs ?? router.options.defaultPendingMinMs
117
- if (pendingMinMs) {
118
- const minPendingPromise = createControlledPromise<void>()
119
- match._nonReactive.minPendingPromise = minPendingPromise
120
- match._forcePending = true
121
-
122
- setTimeout(() => {
123
- minPendingPromise.resolve()
124
- // We've handled the minPendingPromise, so we can delete it
125
- router.updateMatch(match.id, (prev) => {
126
- prev._nonReactive.minPendingPromise = undefined
127
- return {
128
- ...prev,
129
- _forcePending: undefined,
130
- }
131
- })
132
- }, pendingMinMs)
133
- }
134
- }
135
-
136
- function setRouteSsr(match: AnyRouteMatch) {
137
- const route = router.looseRoutesById[match.routeId]
138
- if (route) {
139
- route.options.ssr = match.ssr
140
- }
141
- }
142
- // Right after hydration and before the first render, we need to rehydrate each match
143
- // First step is to reyhdrate loaderData and __beforeLoadContext
144
- let firstNonSsrMatchIndex: number | undefined = undefined
145
- matches.forEach((match) => {
146
- const dehydratedMatch = dehydratedRouter.matches.find(
147
- (d) => d.i === match.id,
148
- )
149
- if (!dehydratedMatch) {
150
- match._nonReactive.dehydrated = false
151
- match.ssr = false
152
- setRouteSsr(match)
153
- return
154
- }
155
-
156
- hydrateMatch(match, dehydratedMatch)
157
- setRouteSsr(match)
158
-
159
- match._nonReactive.dehydrated = match.ssr !== false
160
-
161
- if (match.ssr === 'data-only' || match.ssr === false) {
162
- if (firstNonSsrMatchIndex === undefined) {
163
- firstNonSsrMatchIndex = match.index
164
- setMatchForcePending(match)
165
- }
166
- }
167
- })
168
-
169
- router.stores.setMatches(matches)
170
-
171
- // now that all necessary data is hydrated:
172
- // 1) fully reconstruct the route context
173
- // 2) execute `head()` and `scripts()` for each match
174
- const activeMatches = router.stores.matches.get()
175
- const location = router.stores.location.get()
176
- await Promise.all(
177
- activeMatches.map(async (match) => {
178
- try {
179
- const route = router.looseRoutesById[match.routeId]!
180
-
181
- const parentMatch = activeMatches[match.index - 1]
182
- const parentContext = parentMatch?.context ?? router.options.context
183
-
184
- // `context()` was already executed by `matchRoutes`, however route context was not yet fully reconstructed
185
- // so run it again and merge route context
186
- if (route.options.context) {
187
- const contextFnContext: RouteContextOptions<any, any, any, any, any> =
188
- {
189
- deps: match.loaderDeps,
190
- params: match.params,
191
- context: parentContext ?? {},
192
- location,
193
- navigate: (opts: any) =>
194
- router.navigate({
195
- ...opts,
196
- _fromLocation: location,
197
- }),
198
- buildLocation: router.buildLocation,
199
- cause: match.cause,
200
- abortController: match.abortController,
201
- preload: false,
202
- matches,
203
- routeId: route.id,
204
- }
205
- match.__routeContext =
206
- route.options.context(contextFnContext) ?? undefined
207
- }
208
-
209
- match.context = {
210
- ...parentContext,
211
- ...match.__routeContext,
212
- ...match.__beforeLoadContext,
213
- }
214
-
215
- const assetContext = {
216
- ssr: router.options.ssr,
217
- matches: activeMatches,
218
- match,
219
- params: match.params,
220
- loaderData: match.loaderData,
221
- }
222
- const headFnContent = await route.options.head?.(assetContext)
223
-
224
- const scripts = await route.options.scripts?.(assetContext)
225
-
226
- match.meta = headFnContent?.meta
227
- match.links = headFnContent?.links
228
- match.headScripts = headFnContent?.scripts
229
- match.styles = headFnContent?.styles
230
- match.scripts = scripts
231
- } catch (err) {
232
- if (isNotFound(err)) {
233
- match.error = { isNotFound: true }
234
- console.error(
235
- `NotFound error during hydration for routeId: ${match.routeId}`,
236
- err,
237
- )
238
- } else {
239
- match.error = err as any
240
- console.error(
241
- `Error during hydration for route ${match.routeId}:`,
242
- err,
243
- )
244
- throw err
245
- }
246
- }
247
- }),
248
- )
249
-
250
- const isSpaMode = matches[matches.length - 1]!.id !== lastMatchId
251
- const hasSsrFalseMatches = matches.some((m) => m.ssr === false)
252
- // all matches have data from the server and we are not in SPA mode so we don't need to kick of router.load()
253
- if (!hasSsrFalseMatches && !isSpaMode) {
254
- matches.forEach((match) => {
255
- // remove the dehydrated flag since we won't run router.load() which would remove it
256
- match._nonReactive.dehydrated = undefined
257
- })
258
- // Mark the current location as resolved so that later load cycles
259
- // (e.g. preloads, invalidations) don't mistakenly detect a href change
260
- // (resolvedLocation defaults to undefined and router.load() is skipped
261
- // in the normal SSR hydration path).
262
- router.stores.resolvedLocation.set(router.stores.location.get())
263
- return routeChunkPromise
264
- }
265
-
266
- // schedule router.load() to run after the next tick so we can store the promise in the match before loading starts
267
- const loadPromise = Promise.resolve()
268
- .then(() => router.load())
269
- .catch((err) => {
270
- console.error('Error during router hydration:', err)
271
- })
272
-
273
- // in SPA mode we need to keep the first match below the root route pending until router.load() is finished
274
- // this will prevent that other pending components are rendered but hydration is not blocked
275
- if (isSpaMode) {
276
- const match = matches[1]
277
- if (!match) {
278
- if (process.env.NODE_ENV !== 'production') {
279
- throw new Error(
280
- 'Invariant failed: Expected to find a match below the root match in SPA mode.',
281
- )
282
- }
283
-
284
- invariant()
285
- }
286
- setMatchForcePending(match)
287
-
288
- match._displayPending = true
289
- match._nonReactive.displayPendingPromise = loadPromise
290
-
291
- loadPromise.then(() => {
292
- router.batch(() => {
293
- // ensure router is not in status 'pending' anymore
294
- // this usually happens in Transitioner but if loading synchronously resolves,
295
- // Transitioner won't be rendered while loading so it cannot track the change from loading:true to loading:false
296
- if (router.stores.status.get() === 'pending') {
297
- router.stores.status.set('idle')
298
- router.stores.resolvedLocation.set(router.stores.location.get())
299
- }
300
- // hide the pending component once the load is finished
301
- router.updateMatch(match.id, (prev) => ({
302
- ...prev,
303
- _displayPending: undefined,
304
- displayPendingPromise: undefined,
305
- }))
306
- })
307
- })
308
- }
309
- return routeChunkPromise
310
- }
1
+ export { hydrate } from '../load-client'
@@ -1,7 +1,16 @@
1
1
  export function dehydrateSsrMatchId(id: string): string {
2
- return id.replaceAll('/', '\0')
2
+ return id
3
+ .replaceAll('~', '~~')
4
+ .replaceAll('\0', '~0')
5
+ .replaceAll('\uFFFD', '~r')
6
+ .replaceAll('/', '\0')
3
7
  }
4
8
 
5
9
  export function hydrateSsrMatchId(id: string): string {
6
- return id.replaceAll('\0', '/').replaceAll('\uFFFD', '/')
10
+ return id
11
+ .replaceAll('\0', '/')
12
+ .replaceAll('\uFFFD', '/')
13
+ .replace(/~([~0r])/g, (_, code) =>
14
+ code === '0' ? '\0' : code === 'r' ? '\uFFFD' : code,
15
+ )
7
16
  }
@@ -8,6 +8,7 @@ import {
8
8
  import { decodePath } from '../utils'
9
9
  import { createLRUCache } from '../lru-cache'
10
10
  import { rootRouteId } from '../root'
11
+ import { _getRenderedMatches } from '../load-client'
11
12
  import minifiedTsrBootStrapScript from './tsrScript?script-string'
12
13
  import { GLOBAL_TSR, TSR_SCRIPT_BARRIER_ID } from './constants'
13
14
  import { dehydrateSsrMatchId } from './ssr-match-id'
@@ -51,7 +52,7 @@ export function dehydrateMatch(match: AnyRouteMatch): DehydratedMatch {
51
52
  dehydratedMatch[shorthand] = match[key]
52
53
  }
53
54
  }
54
- if (match.globalNotFound) {
55
+ if (match._notFound) {
55
56
  dehydratedMatch.g = true
56
57
  }
57
58
  return dehydratedMatch
@@ -389,7 +390,7 @@ export function attachRouterServerSsrUtils({
389
390
  if (!manifest) return manifest
390
391
 
391
392
  const requestAssets = getRequestAssets?.()
392
- const matches = router.stores.matches.get()
393
+ const matches = _getRenderedMatches(router.stores.matches.get())
393
394
  const hasAssets = hasRequestAssets(requestAssets)
394
395
 
395
396
  if (!hasAssets && !manifest.inlineCss) {
@@ -495,8 +496,9 @@ export function attachRouterServerSsrUtils({
495
496
 
496
497
  invariant()
497
498
  }
498
- let matchesToDehydrate = router.stores.matches.get()
499
- if (router.isShell()) {
499
+ let matchesToDehydrate = _getRenderedMatches(router.stores.matches.get())
500
+ const isShell = router.isShell()
501
+ if (isShell) {
500
502
  // In SPA mode we only want to dehydrate the root match
501
503
  matchesToDehydrate = matchesToDehydrate.slice(0, 1)
502
504
  }
@@ -540,11 +542,10 @@ export function attachRouterServerSsrUtils({
540
542
  manifest: manifestToDehydrate,
541
543
  matches,
542
544
  }
543
- const lastMatchId = matchesToDehydrate[matchesToDehydrate.length - 1]?.id
544
- if (lastMatchId) {
545
- dehydratedRouter.lastMatchId = dehydrateSsrMatchId(lastMatchId)
546
- }
547
545
  const dehydratedData = await router.options.dehydrate?.()
546
+ if (cleanupStarted) {
547
+ return
548
+ }
548
549
  if (dehydratedData) {
549
550
  dehydratedRouter.dehydratedData = dehydratedData
550
551
  }
@@ -4,6 +4,8 @@ import { TSR_SCRIPT_BARRIER_ID } from './constants'
4
4
  import type { AnyRouter } from '../router'
5
5
 
6
6
  export type TransformStreamWithRouterOptions = {
7
+ /** The request lifetime that owns this response stream. */
8
+ signal?: AbortSignal
7
9
  /** Timeout for serialization to complete after app render finishes (default: 60000ms) */
8
10
  timeoutMs?: number
9
11
  /** Maximum lifetime of the stream transform (default: 120000ms). Safety net for cleanup. */
@@ -196,6 +198,22 @@ function createAbortNotifier(opts?: TransformStreamWithRouterOptions) {
196
198
  }
197
199
  }
198
200
 
201
+ function listenToAbort(
202
+ signal: AbortSignal | undefined,
203
+ onAbort: (reason?: unknown) => void,
204
+ ) {
205
+ if (!signal) {
206
+ return
207
+ }
208
+ if (signal.aborted) {
209
+ onAbort(signal.reason)
210
+ return
211
+ }
212
+ const listener = () => onAbort(signal.reason)
213
+ signal.addEventListener('abort', listener, { once: true })
214
+ return () => signal.removeEventListener('abort', listener)
215
+ }
216
+
199
217
  export function transformStreamWithRouter(
200
218
  router: AnyRouter,
201
219
  appStream: ReadableStream,
@@ -224,6 +242,7 @@ function makeFastPathStream(
224
242
  let controller: ReadableStreamDefaultController<Uint8Array> | undefined
225
243
  let state: MergeState = MergeState.ReadingBody
226
244
  let lifetimeTimeoutHandle: ReturnType<typeof setTimeout> | undefined
245
+ let stopListeningToAbort: (() => void) | undefined
227
246
  let stopListeningToInjectedHtml: (() => void) | undefined
228
247
  const readerState = createReaderState(appStream)
229
248
  const notifyAbort = createAbortNotifier(opts)
@@ -251,6 +270,8 @@ function makeFastPathStream(
251
270
  clearTimeout(lifetimeTimeoutHandle)
252
271
  lifetimeTimeoutHandle = undefined
253
272
  }
273
+ stopListeningToAbort?.()
274
+ stopListeningToAbort = undefined
254
275
  try {
255
276
  stopListeningToInjectedHtml?.()
256
277
  } catch {
@@ -360,6 +381,11 @@ function makeFastPathStream(
360
381
  },
361
382
  })
362
383
 
384
+ stopListeningToAbort = listenToAbort(opts?.signal, (reason) => {
385
+ safeError(reason)
386
+ cleanup(reason)
387
+ })
388
+
363
389
  return stream
364
390
  }
365
391
 
@@ -380,6 +406,7 @@ function makeMainStream(
380
406
  let stopListeningToSerializationFinished: (() => void) | undefined
381
407
  let serializationTimeoutHandle: ReturnType<typeof setTimeout> | undefined
382
408
  let lifetimeTimeoutHandle: ReturnType<typeof setTimeout> | undefined
409
+ let stopListeningToAbort: (() => void) | undefined
383
410
  let cleanedUp = false
384
411
 
385
412
  let controller: ReadableStreamDefaultController<Uint8Array> | undefined
@@ -509,6 +536,8 @@ function makeMainStream(
509
536
  }
510
537
  stopListeningToInjectedHtml = undefined
511
538
  stopListeningToSerializationFinished = undefined
539
+ stopListeningToAbort?.()
540
+ stopListeningToAbort = undefined
512
541
 
513
542
  if (serializationTimeoutHandle !== undefined) {
514
543
  clearTimeout(serializationTimeoutHandle)
@@ -763,7 +792,14 @@ function makeMainStream(
763
792
  if (cleanedUp || isDone()) return stream
764
793
  }
765
794
 
766
- // Transform the appStream
795
+ stopListeningToAbort = listenToAbort(opts?.signal, (reason) => {
796
+ safeError(reason)
797
+ cleanup(reason)
798
+ })
799
+ if (cleanedUp || isDone())
800
+ return stream
801
+
802
+ // Transform the appStream
767
803
  ;(async () => {
768
804
  try {
769
805
  while (true) {
package/src/ssr/types.ts CHANGED
@@ -15,7 +15,6 @@ export interface DehydratedMatch {
15
15
  export interface DehydratedRouter {
16
16
  manifest: Manifest | undefined
17
17
  dehydratedData?: any
18
- lastMatchId?: string
19
18
  matches: Array<DehydratedMatch>
20
19
  }
21
20