@tanstack/router-ssr-query-core 1.169.0 → 1.169.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.
@@ -10,9 +10,39 @@ function setupCoreRouterSsrQueryIntegration({ router, queryClient, dehydrateOpti
10
10
  const sentQueries = /* @__PURE__ */ new Set();
11
11
  const queryStream = createPushableStream();
12
12
  let unsubscribe = void 0;
13
+ let cleanupRegistered = false;
14
+ let tornDown = false;
15
+ const teardown = () => {
16
+ if (tornDown) return;
17
+ tornDown = true;
18
+ try {
19
+ unsubscribe?.();
20
+ } catch {}
21
+ unsubscribe = void 0;
22
+ try {
23
+ if (!queryStream.isClosed()) queryStream.close();
24
+ } catch {}
25
+ try {
26
+ queryClient.cancelQueries();
27
+ } catch {}
28
+ try {
29
+ queryClient.clear();
30
+ } catch {}
31
+ sentQueries.clear();
32
+ };
33
+ const registerCleanup = (serverSsr = router.serverSsr) => {
34
+ if (cleanupRegistered) return;
35
+ if (!serverSsr) return;
36
+ serverSsr.onCleanup(teardown);
37
+ cleanupRegistered = true;
38
+ };
39
+ router.serverSsrLifecycle = {
40
+ ...router.serverSsrLifecycle,
41
+ onServerSsrAttach: [...router.serverSsrLifecycle?.onServerSsrAttach ?? [], registerCleanup]
42
+ };
13
43
  router.options.dehydrate = async () => {
14
44
  router.serverSsr.onRenderFinished(() => {
15
- queryStream.close();
45
+ if (!queryStream.isClosed()) queryStream.close();
16
46
  unsubscribe?.();
17
47
  unsubscribe = void 0;
18
48
  });
@@ -103,13 +133,20 @@ function createPushableStream() {
103
133
  let _isClosed = false;
104
134
  return {
105
135
  stream,
106
- enqueue: (chunk) => controllerRef.enqueue(chunk),
136
+ enqueue: (chunk) => {
137
+ if (!_isClosed) controllerRef.enqueue(chunk);
138
+ },
107
139
  close: () => {
140
+ if (_isClosed) return;
108
141
  controllerRef.close();
109
142
  _isClosed = true;
110
143
  },
111
144
  isClosed: () => _isClosed,
112
- error: (err) => controllerRef.error(err)
145
+ error: (err) => {
146
+ if (_isClosed) return;
147
+ _isClosed = true;
148
+ controllerRef.error(err);
149
+ }
113
150
  };
114
151
  }
115
152
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../src/index.ts"],"sourcesContent":["import {\n dehydrate as queryDehydrate,\n hydrate as queryHydrate,\n} from '@tanstack/query-core'\nimport { isRedirect } from '@tanstack/router-core'\nimport { isServer } from '@tanstack/router-core/isServer'\nimport type { AnyRouter } from '@tanstack/router-core'\nimport type {\n DehydrateOptions,\n HydrateOptions,\n QueryClient,\n DehydratedState as QueryDehydratedState,\n} from '@tanstack/query-core'\n\nexport type RouterSsrQueryOptions<TRouter extends AnyRouter> = {\n router: TRouter\n queryClient: QueryClient\n dehydrateOptions?: DehydrateOptions\n hydrateOptions?: HydrateOptions\n\n /**\n * If `true`, the QueryClient will handle errors thrown by `redirect()` inside of mutations and queries.\n *\n * @default true\n * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/api/router/redirectFunction)\n */\n handleRedirects?: boolean\n}\n\ntype DehydratedRouterQueryState = {\n dehydratedQueryClient?: QueryDehydratedState\n queryStream: ReadableStream<QueryDehydratedState>\n}\n\nexport function setupCoreRouterSsrQueryIntegration<TRouter extends AnyRouter>({\n router,\n queryClient,\n dehydrateOptions,\n hydrateOptions,\n handleRedirects = true,\n}: RouterSsrQueryOptions<TRouter>) {\n const ogHydrate = router.options.hydrate\n const ogDehydrate = router.options.dehydrate\n\n if (isServer ?? router.isServer) {\n const sentQueries = new Set<string>()\n const queryStream = createPushableStream()\n let unsubscribe: (() => void) | undefined = undefined\n router.options.dehydrate =\n async (): Promise<DehydratedRouterQueryState> => {\n router.serverSsr!.onRenderFinished(() => {\n queryStream.close()\n unsubscribe?.()\n unsubscribe = undefined\n })\n const ogDehydrated = await ogDehydrate?.()\n\n const dehydratedRouter = {\n ...ogDehydrated,\n // prepare the stream for queries coming up during rendering\n queryStream: queryStream.stream,\n }\n\n const dehydratedQueryClient = queryDehydrate(\n queryClient,\n dehydrateOptions,\n )\n if (dehydratedQueryClient.queries.length > 0) {\n dehydratedQueryClient.queries.forEach((query) => {\n sentQueries.add(query.queryHash)\n })\n dehydratedRouter.dehydratedQueryClient = dehydratedQueryClient\n }\n\n return dehydratedRouter\n }\n\n const ogClientOptions = queryClient.getDefaultOptions()\n queryClient.setDefaultOptions({\n ...ogClientOptions,\n dehydrate: {\n shouldDehydrateQuery: () => true,\n ...ogClientOptions.dehydrate,\n },\n })\n\n unsubscribe = queryClient.getQueryCache().subscribe((event) => {\n // before rendering starts, we do not stream individual queries\n // instead we dehydrate the entire query client in router's dehydrate()\n // if attachRouterServerSsrUtils() has not been called yet, `router.serverSsr` will be undefined and we also do not stream\n if (!router.serverSsr?.isDehydrated()) {\n return\n }\n if (sentQueries.has(event.query.queryHash)) {\n return\n }\n // promise not yet set on the query, so we cannot stream it yet\n if (!event.query.promise) {\n return\n }\n if (queryStream.isClosed()) {\n console.warn(\n `tried to stream query ${event.query.queryHash} after stream was already closed`,\n )\n return\n }\n const dehydratedQuery = queryDehydrate(queryClient, {\n ...dehydrateOptions,\n shouldDehydrateQuery: (query) => {\n if (query.queryHash !== event.query.queryHash) {\n return false\n }\n\n return (\n (ogClientOptions.dehydrate?.shouldDehydrateQuery?.(query) ??\n true) &&\n (dehydrateOptions?.shouldDehydrateQuery?.(query) ?? true)\n )\n },\n })\n\n if (dehydratedQuery.queries.length === 0) {\n return\n }\n\n sentQueries.add(event.query.queryHash)\n queryStream.enqueue(dehydratedQuery)\n })\n // on the client\n } else {\n router.options.hydrate = async (dehydrated: DehydratedRouterQueryState) => {\n await ogHydrate?.(dehydrated)\n // hydrate the query client with the dehydrated data (if it was dehydrated on the server)\n if (dehydrated.dehydratedQueryClient) {\n queryHydrate(\n queryClient,\n dehydrated.dehydratedQueryClient,\n hydrateOptions,\n )\n }\n\n // read the query stream and hydrate the queries as they come in\n const reader = dehydrated.queryStream.getReader()\n reader\n .read()\n .then(async function handle({ done, value }) {\n queryHydrate(queryClient, value, hydrateOptions)\n if (done) {\n return\n }\n const result = await reader.read()\n return handle(result)\n })\n .catch((err) => {\n console.error('Error reading query stream:', err)\n })\n }\n if (handleRedirects) {\n const ogMutationCacheConfig = queryClient.getMutationCache().config\n queryClient.getMutationCache().config = {\n ...ogMutationCacheConfig,\n onError: (error, ...rest) => {\n if (isRedirect(error)) {\n error.options._fromLocation = router.stores.location.get()\n return router.navigate(router.resolveRedirect(error).options)\n }\n\n return ogMutationCacheConfig.onError?.(error, ...rest)\n },\n }\n\n const ogQueryCacheConfig = queryClient.getQueryCache().config\n queryClient.getQueryCache().config = {\n ...ogQueryCacheConfig,\n onError: (error, ...rest) => {\n if (isRedirect(error)) {\n error.options._fromLocation = router.stores.location.get()\n return router.navigate(router.resolveRedirect(error).options)\n }\n\n return ogQueryCacheConfig.onError?.(error, ...rest)\n },\n }\n }\n }\n}\n\ntype PushableStream = {\n stream: ReadableStream\n enqueue: (chunk: unknown) => void\n close: () => void\n isClosed: () => boolean\n error: (err: unknown) => void\n}\n\nfunction createPushableStream(): PushableStream {\n let controllerRef: ReadableStreamDefaultController\n const stream = new ReadableStream({\n start(controller) {\n controllerRef = controller\n },\n })\n let _isClosed = false\n\n return {\n stream,\n enqueue: (chunk) => controllerRef.enqueue(chunk),\n close: () => {\n controllerRef.close()\n _isClosed = true\n },\n isClosed: () => _isClosed,\n error: (err: unknown) => controllerRef.error(err),\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../src/index.ts"],"sourcesContent":["import {\n dehydrate as queryDehydrate,\n hydrate as queryHydrate,\n} from '@tanstack/query-core'\nimport { isRedirect } from '@tanstack/router-core'\nimport { isServer } from '@tanstack/router-core/isServer'\nimport type { AnyRouter } from '@tanstack/router-core'\nimport type {\n DehydrateOptions,\n HydrateOptions,\n QueryClient,\n DehydratedState as QueryDehydratedState,\n} from '@tanstack/query-core'\n\nexport type RouterSsrQueryOptions<TRouter extends AnyRouter> = {\n router: TRouter\n queryClient: QueryClient\n dehydrateOptions?: DehydrateOptions\n hydrateOptions?: HydrateOptions\n\n /**\n * If `true`, the QueryClient will handle errors thrown by `redirect()` inside of mutations and queries.\n *\n * @default true\n * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/api/router/redirectFunction)\n */\n handleRedirects?: boolean\n}\n\ntype DehydratedRouterQueryState = {\n dehydratedQueryClient?: QueryDehydratedState\n queryStream: ReadableStream<QueryDehydratedState>\n}\n\nexport function setupCoreRouterSsrQueryIntegration<TRouter extends AnyRouter>({\n router,\n queryClient,\n dehydrateOptions,\n hydrateOptions,\n handleRedirects = true,\n}: RouterSsrQueryOptions<TRouter>) {\n const ogHydrate = router.options.hydrate\n const ogDehydrate = router.options.dehydrate\n\n if (isServer ?? router.isServer) {\n const sentQueries = new Set<string>()\n const queryStream = createPushableStream()\n let unsubscribe: (() => void) | undefined = undefined\n let cleanupRegistered = false\n let tornDown = false\n\n const teardown = () => {\n if (tornDown) return\n tornDown = true\n try {\n unsubscribe?.()\n } catch {\n // ignore\n }\n unsubscribe = undefined\n try {\n if (!queryStream.isClosed()) queryStream.close()\n } catch {\n // ignore\n }\n // Cancel any in-flight queries and clear the cache. Removing queries\n // cancels their gcTime setTimeout handles which would otherwise pin\n // the queryClient (and transitively the router via router.context)\n // alive for the full gcTime window (default 5min) per SSR request.\n try {\n queryClient.cancelQueries()\n } catch {\n // ignore\n }\n try {\n queryClient.clear()\n } catch {\n // ignore\n }\n sentQueries.clear()\n }\n\n // Register teardown as soon as SSR attaches. attachRouterServerSsrUtils()\n // runs before router.load(), so this covers redirects/errors thrown before\n // router.options.dehydrate() can run.\n const registerCleanup = (serverSsr = router.serverSsr) => {\n if (cleanupRegistered) return\n if (!serverSsr) return\n serverSsr.onCleanup(teardown)\n cleanupRegistered = true\n }\n router.serverSsrLifecycle = {\n ...router.serverSsrLifecycle,\n onServerSsrAttach: [\n ...(router.serverSsrLifecycle?.onServerSsrAttach ?? []),\n registerCleanup,\n ],\n }\n\n router.options.dehydrate =\n async (): Promise<DehydratedRouterQueryState> => {\n router.serverSsr!.onRenderFinished(() => {\n if (!queryStream.isClosed()) queryStream.close()\n unsubscribe?.()\n unsubscribe = undefined\n })\n const ogDehydrated = await ogDehydrate?.()\n\n const dehydratedRouter = {\n ...ogDehydrated,\n // prepare the stream for queries coming up during rendering\n queryStream: queryStream.stream,\n }\n\n const dehydratedQueryClient = queryDehydrate(\n queryClient,\n dehydrateOptions,\n )\n if (dehydratedQueryClient.queries.length > 0) {\n dehydratedQueryClient.queries.forEach((query) => {\n sentQueries.add(query.queryHash)\n })\n dehydratedRouter.dehydratedQueryClient = dehydratedQueryClient\n }\n\n return dehydratedRouter\n }\n\n const ogClientOptions = queryClient.getDefaultOptions()\n queryClient.setDefaultOptions({\n ...ogClientOptions,\n dehydrate: {\n shouldDehydrateQuery: () => true,\n ...ogClientOptions.dehydrate,\n },\n })\n\n unsubscribe = queryClient.getQueryCache().subscribe((event) => {\n // before rendering starts, we do not stream individual queries\n // instead we dehydrate the entire query client in router's dehydrate()\n // if attachRouterServerSsrUtils() has not been called yet, `router.serverSsr` will be undefined and we also do not stream\n if (!router.serverSsr?.isDehydrated()) {\n return\n }\n if (sentQueries.has(event.query.queryHash)) {\n return\n }\n // promise not yet set on the query, so we cannot stream it yet\n if (!event.query.promise) {\n return\n }\n if (queryStream.isClosed()) {\n console.warn(\n `tried to stream query ${event.query.queryHash} after stream was already closed`,\n )\n return\n }\n const dehydratedQuery = queryDehydrate(queryClient, {\n ...dehydrateOptions,\n shouldDehydrateQuery: (query) => {\n if (query.queryHash !== event.query.queryHash) {\n return false\n }\n\n return (\n (ogClientOptions.dehydrate?.shouldDehydrateQuery?.(query) ??\n true) &&\n (dehydrateOptions?.shouldDehydrateQuery?.(query) ?? true)\n )\n },\n })\n\n if (dehydratedQuery.queries.length === 0) {\n return\n }\n\n sentQueries.add(event.query.queryHash)\n queryStream.enqueue(dehydratedQuery)\n })\n // on the client\n } else {\n router.options.hydrate = async (dehydrated: DehydratedRouterQueryState) => {\n await ogHydrate?.(dehydrated)\n // hydrate the query client with the dehydrated data (if it was dehydrated on the server)\n if (dehydrated.dehydratedQueryClient) {\n queryHydrate(\n queryClient,\n dehydrated.dehydratedQueryClient,\n hydrateOptions,\n )\n }\n\n // read the query stream and hydrate the queries as they come in\n const reader = dehydrated.queryStream.getReader()\n reader\n .read()\n .then(async function handle({ done, value }) {\n queryHydrate(queryClient, value, hydrateOptions)\n if (done) {\n return\n }\n const result = await reader.read()\n return handle(result)\n })\n .catch((err) => {\n console.error('Error reading query stream:', err)\n })\n }\n if (handleRedirects) {\n const ogMutationCacheConfig = queryClient.getMutationCache().config\n queryClient.getMutationCache().config = {\n ...ogMutationCacheConfig,\n onError: (error, ...rest) => {\n if (isRedirect(error)) {\n error.options._fromLocation = router.stores.location.get()\n return router.navigate(router.resolveRedirect(error).options)\n }\n\n return ogMutationCacheConfig.onError?.(error, ...rest)\n },\n }\n\n const ogQueryCacheConfig = queryClient.getQueryCache().config\n queryClient.getQueryCache().config = {\n ...ogQueryCacheConfig,\n onError: (error, ...rest) => {\n if (isRedirect(error)) {\n error.options._fromLocation = router.stores.location.get()\n return router.navigate(router.resolveRedirect(error).options)\n }\n\n return ogQueryCacheConfig.onError?.(error, ...rest)\n },\n }\n }\n }\n}\n\ntype PushableStream = {\n stream: ReadableStream\n enqueue: (chunk: unknown) => void\n close: () => void\n isClosed: () => boolean\n error: (err: unknown) => void\n}\n\nfunction createPushableStream(): PushableStream {\n let controllerRef: ReadableStreamDefaultController\n const stream = new ReadableStream({\n start(controller) {\n controllerRef = controller\n },\n })\n let _isClosed = false\n\n return {\n stream,\n enqueue: (chunk) => {\n if (!_isClosed) controllerRef.enqueue(chunk)\n },\n close: () => {\n if (_isClosed) return\n controllerRef.close()\n _isClosed = true\n },\n isClosed: () => _isClosed,\n error: (err: unknown) => {\n if (_isClosed) return\n _isClosed = true\n controllerRef.error(err)\n },\n }\n}\n"],"mappings":"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"}
package/dist/esm/index.js CHANGED
@@ -9,9 +9,39 @@ function setupCoreRouterSsrQueryIntegration({ router, queryClient, dehydrateOpti
9
9
  const sentQueries = /* @__PURE__ */ new Set();
10
10
  const queryStream = createPushableStream();
11
11
  let unsubscribe = void 0;
12
+ let cleanupRegistered = false;
13
+ let tornDown = false;
14
+ const teardown = () => {
15
+ if (tornDown) return;
16
+ tornDown = true;
17
+ try {
18
+ unsubscribe?.();
19
+ } catch {}
20
+ unsubscribe = void 0;
21
+ try {
22
+ if (!queryStream.isClosed()) queryStream.close();
23
+ } catch {}
24
+ try {
25
+ queryClient.cancelQueries();
26
+ } catch {}
27
+ try {
28
+ queryClient.clear();
29
+ } catch {}
30
+ sentQueries.clear();
31
+ };
32
+ const registerCleanup = (serverSsr = router.serverSsr) => {
33
+ if (cleanupRegistered) return;
34
+ if (!serverSsr) return;
35
+ serverSsr.onCleanup(teardown);
36
+ cleanupRegistered = true;
37
+ };
38
+ router.serverSsrLifecycle = {
39
+ ...router.serverSsrLifecycle,
40
+ onServerSsrAttach: [...router.serverSsrLifecycle?.onServerSsrAttach ?? [], registerCleanup]
41
+ };
12
42
  router.options.dehydrate = async () => {
13
43
  router.serverSsr.onRenderFinished(() => {
14
- queryStream.close();
44
+ if (!queryStream.isClosed()) queryStream.close();
15
45
  unsubscribe?.();
16
46
  unsubscribe = void 0;
17
47
  });
@@ -102,13 +132,20 @@ function createPushableStream() {
102
132
  let _isClosed = false;
103
133
  return {
104
134
  stream,
105
- enqueue: (chunk) => controllerRef.enqueue(chunk),
135
+ enqueue: (chunk) => {
136
+ if (!_isClosed) controllerRef.enqueue(chunk);
137
+ },
106
138
  close: () => {
139
+ if (_isClosed) return;
107
140
  controllerRef.close();
108
141
  _isClosed = true;
109
142
  },
110
143
  isClosed: () => _isClosed,
111
- error: (err) => controllerRef.error(err)
144
+ error: (err) => {
145
+ if (_isClosed) return;
146
+ _isClosed = true;
147
+ controllerRef.error(err);
148
+ }
112
149
  };
113
150
  }
114
151
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/index.ts"],"sourcesContent":["import {\n dehydrate as queryDehydrate,\n hydrate as queryHydrate,\n} from '@tanstack/query-core'\nimport { isRedirect } from '@tanstack/router-core'\nimport { isServer } from '@tanstack/router-core/isServer'\nimport type { AnyRouter } from '@tanstack/router-core'\nimport type {\n DehydrateOptions,\n HydrateOptions,\n QueryClient,\n DehydratedState as QueryDehydratedState,\n} from '@tanstack/query-core'\n\nexport type RouterSsrQueryOptions<TRouter extends AnyRouter> = {\n router: TRouter\n queryClient: QueryClient\n dehydrateOptions?: DehydrateOptions\n hydrateOptions?: HydrateOptions\n\n /**\n * If `true`, the QueryClient will handle errors thrown by `redirect()` inside of mutations and queries.\n *\n * @default true\n * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/api/router/redirectFunction)\n */\n handleRedirects?: boolean\n}\n\ntype DehydratedRouterQueryState = {\n dehydratedQueryClient?: QueryDehydratedState\n queryStream: ReadableStream<QueryDehydratedState>\n}\n\nexport function setupCoreRouterSsrQueryIntegration<TRouter extends AnyRouter>({\n router,\n queryClient,\n dehydrateOptions,\n hydrateOptions,\n handleRedirects = true,\n}: RouterSsrQueryOptions<TRouter>) {\n const ogHydrate = router.options.hydrate\n const ogDehydrate = router.options.dehydrate\n\n if (isServer ?? router.isServer) {\n const sentQueries = new Set<string>()\n const queryStream = createPushableStream()\n let unsubscribe: (() => void) | undefined = undefined\n router.options.dehydrate =\n async (): Promise<DehydratedRouterQueryState> => {\n router.serverSsr!.onRenderFinished(() => {\n queryStream.close()\n unsubscribe?.()\n unsubscribe = undefined\n })\n const ogDehydrated = await ogDehydrate?.()\n\n const dehydratedRouter = {\n ...ogDehydrated,\n // prepare the stream for queries coming up during rendering\n queryStream: queryStream.stream,\n }\n\n const dehydratedQueryClient = queryDehydrate(\n queryClient,\n dehydrateOptions,\n )\n if (dehydratedQueryClient.queries.length > 0) {\n dehydratedQueryClient.queries.forEach((query) => {\n sentQueries.add(query.queryHash)\n })\n dehydratedRouter.dehydratedQueryClient = dehydratedQueryClient\n }\n\n return dehydratedRouter\n }\n\n const ogClientOptions = queryClient.getDefaultOptions()\n queryClient.setDefaultOptions({\n ...ogClientOptions,\n dehydrate: {\n shouldDehydrateQuery: () => true,\n ...ogClientOptions.dehydrate,\n },\n })\n\n unsubscribe = queryClient.getQueryCache().subscribe((event) => {\n // before rendering starts, we do not stream individual queries\n // instead we dehydrate the entire query client in router's dehydrate()\n // if attachRouterServerSsrUtils() has not been called yet, `router.serverSsr` will be undefined and we also do not stream\n if (!router.serverSsr?.isDehydrated()) {\n return\n }\n if (sentQueries.has(event.query.queryHash)) {\n return\n }\n // promise not yet set on the query, so we cannot stream it yet\n if (!event.query.promise) {\n return\n }\n if (queryStream.isClosed()) {\n console.warn(\n `tried to stream query ${event.query.queryHash} after stream was already closed`,\n )\n return\n }\n const dehydratedQuery = queryDehydrate(queryClient, {\n ...dehydrateOptions,\n shouldDehydrateQuery: (query) => {\n if (query.queryHash !== event.query.queryHash) {\n return false\n }\n\n return (\n (ogClientOptions.dehydrate?.shouldDehydrateQuery?.(query) ??\n true) &&\n (dehydrateOptions?.shouldDehydrateQuery?.(query) ?? true)\n )\n },\n })\n\n if (dehydratedQuery.queries.length === 0) {\n return\n }\n\n sentQueries.add(event.query.queryHash)\n queryStream.enqueue(dehydratedQuery)\n })\n // on the client\n } else {\n router.options.hydrate = async (dehydrated: DehydratedRouterQueryState) => {\n await ogHydrate?.(dehydrated)\n // hydrate the query client with the dehydrated data (if it was dehydrated on the server)\n if (dehydrated.dehydratedQueryClient) {\n queryHydrate(\n queryClient,\n dehydrated.dehydratedQueryClient,\n hydrateOptions,\n )\n }\n\n // read the query stream and hydrate the queries as they come in\n const reader = dehydrated.queryStream.getReader()\n reader\n .read()\n .then(async function handle({ done, value }) {\n queryHydrate(queryClient, value, hydrateOptions)\n if (done) {\n return\n }\n const result = await reader.read()\n return handle(result)\n })\n .catch((err) => {\n console.error('Error reading query stream:', err)\n })\n }\n if (handleRedirects) {\n const ogMutationCacheConfig = queryClient.getMutationCache().config\n queryClient.getMutationCache().config = {\n ...ogMutationCacheConfig,\n onError: (error, ...rest) => {\n if (isRedirect(error)) {\n error.options._fromLocation = router.stores.location.get()\n return router.navigate(router.resolveRedirect(error).options)\n }\n\n return ogMutationCacheConfig.onError?.(error, ...rest)\n },\n }\n\n const ogQueryCacheConfig = queryClient.getQueryCache().config\n queryClient.getQueryCache().config = {\n ...ogQueryCacheConfig,\n onError: (error, ...rest) => {\n if (isRedirect(error)) {\n error.options._fromLocation = router.stores.location.get()\n return router.navigate(router.resolveRedirect(error).options)\n }\n\n return ogQueryCacheConfig.onError?.(error, ...rest)\n },\n }\n }\n }\n}\n\ntype PushableStream = {\n stream: ReadableStream\n enqueue: (chunk: unknown) => void\n close: () => void\n isClosed: () => boolean\n error: (err: unknown) => void\n}\n\nfunction createPushableStream(): PushableStream {\n let controllerRef: ReadableStreamDefaultController\n const stream = new ReadableStream({\n start(controller) {\n controllerRef = controller\n },\n })\n let _isClosed = false\n\n return {\n stream,\n enqueue: (chunk) => controllerRef.enqueue(chunk),\n close: () => {\n controllerRef.close()\n _isClosed = true\n },\n isClosed: () => _isClosed,\n error: (err: unknown) => controllerRef.error(err),\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/index.ts"],"sourcesContent":["import {\n dehydrate as queryDehydrate,\n hydrate as queryHydrate,\n} from '@tanstack/query-core'\nimport { isRedirect } from '@tanstack/router-core'\nimport { isServer } from '@tanstack/router-core/isServer'\nimport type { AnyRouter } from '@tanstack/router-core'\nimport type {\n DehydrateOptions,\n HydrateOptions,\n QueryClient,\n DehydratedState as QueryDehydratedState,\n} from '@tanstack/query-core'\n\nexport type RouterSsrQueryOptions<TRouter extends AnyRouter> = {\n router: TRouter\n queryClient: QueryClient\n dehydrateOptions?: DehydrateOptions\n hydrateOptions?: HydrateOptions\n\n /**\n * If `true`, the QueryClient will handle errors thrown by `redirect()` inside of mutations and queries.\n *\n * @default true\n * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/api/router/redirectFunction)\n */\n handleRedirects?: boolean\n}\n\ntype DehydratedRouterQueryState = {\n dehydratedQueryClient?: QueryDehydratedState\n queryStream: ReadableStream<QueryDehydratedState>\n}\n\nexport function setupCoreRouterSsrQueryIntegration<TRouter extends AnyRouter>({\n router,\n queryClient,\n dehydrateOptions,\n hydrateOptions,\n handleRedirects = true,\n}: RouterSsrQueryOptions<TRouter>) {\n const ogHydrate = router.options.hydrate\n const ogDehydrate = router.options.dehydrate\n\n if (isServer ?? router.isServer) {\n const sentQueries = new Set<string>()\n const queryStream = createPushableStream()\n let unsubscribe: (() => void) | undefined = undefined\n let cleanupRegistered = false\n let tornDown = false\n\n const teardown = () => {\n if (tornDown) return\n tornDown = true\n try {\n unsubscribe?.()\n } catch {\n // ignore\n }\n unsubscribe = undefined\n try {\n if (!queryStream.isClosed()) queryStream.close()\n } catch {\n // ignore\n }\n // Cancel any in-flight queries and clear the cache. Removing queries\n // cancels their gcTime setTimeout handles which would otherwise pin\n // the queryClient (and transitively the router via router.context)\n // alive for the full gcTime window (default 5min) per SSR request.\n try {\n queryClient.cancelQueries()\n } catch {\n // ignore\n }\n try {\n queryClient.clear()\n } catch {\n // ignore\n }\n sentQueries.clear()\n }\n\n // Register teardown as soon as SSR attaches. attachRouterServerSsrUtils()\n // runs before router.load(), so this covers redirects/errors thrown before\n // router.options.dehydrate() can run.\n const registerCleanup = (serverSsr = router.serverSsr) => {\n if (cleanupRegistered) return\n if (!serverSsr) return\n serverSsr.onCleanup(teardown)\n cleanupRegistered = true\n }\n router.serverSsrLifecycle = {\n ...router.serverSsrLifecycle,\n onServerSsrAttach: [\n ...(router.serverSsrLifecycle?.onServerSsrAttach ?? []),\n registerCleanup,\n ],\n }\n\n router.options.dehydrate =\n async (): Promise<DehydratedRouterQueryState> => {\n router.serverSsr!.onRenderFinished(() => {\n if (!queryStream.isClosed()) queryStream.close()\n unsubscribe?.()\n unsubscribe = undefined\n })\n const ogDehydrated = await ogDehydrate?.()\n\n const dehydratedRouter = {\n ...ogDehydrated,\n // prepare the stream for queries coming up during rendering\n queryStream: queryStream.stream,\n }\n\n const dehydratedQueryClient = queryDehydrate(\n queryClient,\n dehydrateOptions,\n )\n if (dehydratedQueryClient.queries.length > 0) {\n dehydratedQueryClient.queries.forEach((query) => {\n sentQueries.add(query.queryHash)\n })\n dehydratedRouter.dehydratedQueryClient = dehydratedQueryClient\n }\n\n return dehydratedRouter\n }\n\n const ogClientOptions = queryClient.getDefaultOptions()\n queryClient.setDefaultOptions({\n ...ogClientOptions,\n dehydrate: {\n shouldDehydrateQuery: () => true,\n ...ogClientOptions.dehydrate,\n },\n })\n\n unsubscribe = queryClient.getQueryCache().subscribe((event) => {\n // before rendering starts, we do not stream individual queries\n // instead we dehydrate the entire query client in router's dehydrate()\n // if attachRouterServerSsrUtils() has not been called yet, `router.serverSsr` will be undefined and we also do not stream\n if (!router.serverSsr?.isDehydrated()) {\n return\n }\n if (sentQueries.has(event.query.queryHash)) {\n return\n }\n // promise not yet set on the query, so we cannot stream it yet\n if (!event.query.promise) {\n return\n }\n if (queryStream.isClosed()) {\n console.warn(\n `tried to stream query ${event.query.queryHash} after stream was already closed`,\n )\n return\n }\n const dehydratedQuery = queryDehydrate(queryClient, {\n ...dehydrateOptions,\n shouldDehydrateQuery: (query) => {\n if (query.queryHash !== event.query.queryHash) {\n return false\n }\n\n return (\n (ogClientOptions.dehydrate?.shouldDehydrateQuery?.(query) ??\n true) &&\n (dehydrateOptions?.shouldDehydrateQuery?.(query) ?? true)\n )\n },\n })\n\n if (dehydratedQuery.queries.length === 0) {\n return\n }\n\n sentQueries.add(event.query.queryHash)\n queryStream.enqueue(dehydratedQuery)\n })\n // on the client\n } else {\n router.options.hydrate = async (dehydrated: DehydratedRouterQueryState) => {\n await ogHydrate?.(dehydrated)\n // hydrate the query client with the dehydrated data (if it was dehydrated on the server)\n if (dehydrated.dehydratedQueryClient) {\n queryHydrate(\n queryClient,\n dehydrated.dehydratedQueryClient,\n hydrateOptions,\n )\n }\n\n // read the query stream and hydrate the queries as they come in\n const reader = dehydrated.queryStream.getReader()\n reader\n .read()\n .then(async function handle({ done, value }) {\n queryHydrate(queryClient, value, hydrateOptions)\n if (done) {\n return\n }\n const result = await reader.read()\n return handle(result)\n })\n .catch((err) => {\n console.error('Error reading query stream:', err)\n })\n }\n if (handleRedirects) {\n const ogMutationCacheConfig = queryClient.getMutationCache().config\n queryClient.getMutationCache().config = {\n ...ogMutationCacheConfig,\n onError: (error, ...rest) => {\n if (isRedirect(error)) {\n error.options._fromLocation = router.stores.location.get()\n return router.navigate(router.resolveRedirect(error).options)\n }\n\n return ogMutationCacheConfig.onError?.(error, ...rest)\n },\n }\n\n const ogQueryCacheConfig = queryClient.getQueryCache().config\n queryClient.getQueryCache().config = {\n ...ogQueryCacheConfig,\n onError: (error, ...rest) => {\n if (isRedirect(error)) {\n error.options._fromLocation = router.stores.location.get()\n return router.navigate(router.resolveRedirect(error).options)\n }\n\n return ogQueryCacheConfig.onError?.(error, ...rest)\n },\n }\n }\n }\n}\n\ntype PushableStream = {\n stream: ReadableStream\n enqueue: (chunk: unknown) => void\n close: () => void\n isClosed: () => boolean\n error: (err: unknown) => void\n}\n\nfunction createPushableStream(): PushableStream {\n let controllerRef: ReadableStreamDefaultController\n const stream = new ReadableStream({\n start(controller) {\n controllerRef = controller\n },\n })\n let _isClosed = false\n\n return {\n stream,\n enqueue: (chunk) => {\n if (!_isClosed) controllerRef.enqueue(chunk)\n },\n close: () => {\n if (_isClosed) return\n controllerRef.close()\n _isClosed = true\n },\n isClosed: () => _isClosed,\n error: (err: unknown) => {\n if (_isClosed) return\n _isClosed = true\n controllerRef.error(err)\n },\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/router-ssr-query-core",
3
- "version": "1.169.0",
3
+ "version": "1.169.1",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -49,7 +49,7 @@
49
49
  "node": ">=20.19"
50
50
  },
51
51
  "devDependencies": {
52
- "@tanstack/router-core": ">=1.170.0",
52
+ "@tanstack/router-core": ">=1.171.7",
53
53
  "@tanstack/query-core": ">=5.90.0",
54
54
  "vite": "*"
55
55
  },
@@ -67,7 +67,7 @@
67
67
  "test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js",
68
68
  "test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js",
69
69
  "test:types:ts60": "tsc",
70
- "test:unit": "exit 0; vitest",
70
+ "test:unit": "vitest",
71
71
  "test:unit:dev": "pnpm run test:unit --watch",
72
72
  "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
73
73
  "build": "vite build"
package/src/index.ts CHANGED
@@ -46,10 +46,61 @@ export function setupCoreRouterSsrQueryIntegration<TRouter extends AnyRouter>({
46
46
  const sentQueries = new Set<string>()
47
47
  const queryStream = createPushableStream()
48
48
  let unsubscribe: (() => void) | undefined = undefined
49
+ let cleanupRegistered = false
50
+ let tornDown = false
51
+
52
+ const teardown = () => {
53
+ if (tornDown) return
54
+ tornDown = true
55
+ try {
56
+ unsubscribe?.()
57
+ } catch {
58
+ // ignore
59
+ }
60
+ unsubscribe = undefined
61
+ try {
62
+ if (!queryStream.isClosed()) queryStream.close()
63
+ } catch {
64
+ // ignore
65
+ }
66
+ // Cancel any in-flight queries and clear the cache. Removing queries
67
+ // cancels their gcTime setTimeout handles which would otherwise pin
68
+ // the queryClient (and transitively the router via router.context)
69
+ // alive for the full gcTime window (default 5min) per SSR request.
70
+ try {
71
+ queryClient.cancelQueries()
72
+ } catch {
73
+ // ignore
74
+ }
75
+ try {
76
+ queryClient.clear()
77
+ } catch {
78
+ // ignore
79
+ }
80
+ sentQueries.clear()
81
+ }
82
+
83
+ // Register teardown as soon as SSR attaches. attachRouterServerSsrUtils()
84
+ // runs before router.load(), so this covers redirects/errors thrown before
85
+ // router.options.dehydrate() can run.
86
+ const registerCleanup = (serverSsr = router.serverSsr) => {
87
+ if (cleanupRegistered) return
88
+ if (!serverSsr) return
89
+ serverSsr.onCleanup(teardown)
90
+ cleanupRegistered = true
91
+ }
92
+ router.serverSsrLifecycle = {
93
+ ...router.serverSsrLifecycle,
94
+ onServerSsrAttach: [
95
+ ...(router.serverSsrLifecycle?.onServerSsrAttach ?? []),
96
+ registerCleanup,
97
+ ],
98
+ }
99
+
49
100
  router.options.dehydrate =
50
101
  async (): Promise<DehydratedRouterQueryState> => {
51
102
  router.serverSsr!.onRenderFinished(() => {
52
- queryStream.close()
103
+ if (!queryStream.isClosed()) queryStream.close()
53
104
  unsubscribe?.()
54
105
  unsubscribe = undefined
55
106
  })
@@ -204,12 +255,19 @@ function createPushableStream(): PushableStream {
204
255
 
205
256
  return {
206
257
  stream,
207
- enqueue: (chunk) => controllerRef.enqueue(chunk),
258
+ enqueue: (chunk) => {
259
+ if (!_isClosed) controllerRef.enqueue(chunk)
260
+ },
208
261
  close: () => {
262
+ if (_isClosed) return
209
263
  controllerRef.close()
210
264
  _isClosed = true
211
265
  },
212
266
  isClosed: () => _isClosed,
213
- error: (err: unknown) => controllerRef.error(err),
267
+ error: (err: unknown) => {
268
+ if (_isClosed) return
269
+ _isClosed = true
270
+ controllerRef.error(err)
271
+ },
214
272
  }
215
273
  }