Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Too many Ably client connections on dev with Next JS #1779

Open
grantsingleton opened this issue Jun 1, 2024 · 9 comments
Open

Too many Ably client connections on dev with Next JS #1779

grantsingleton opened this issue Jun 1, 2024 · 9 comments
Labels
documentation Improvements or additions to public interface documentation (API reference or readme). enhancement New feature or improved functionality.

Comments

@grantsingleton
Copy link

grantsingleton commented Jun 1, 2024

I first initialize ably in my layout.

// ably-provider.tsx
'use client';

import * as Ably from 'ably';
import { AblyProvider } from 'ably/react';

interface AblyProviderProps {
  children: React.ReactNode;
}

export default function AblyRealtimeProvider({ children }: AblyProviderProps) {
  const client = new Ably.Realtime({ key: process.env.NEXT_PUBLIC_ABLY_CLIENT_API_KEY });

  return <AblyProvider client={client} key='ably'>{children}</AblyProvider>;
}
// layout.tsx
import AuthProvider from 'components/auth/provider';
import ThemeProvider from 'components/theme/provider';
import Navigation from 'components/navigation';
import AblyRealtimeProvider from '@/components/ably/provider';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {

  return (
    <AuthProvider>
      <AblyRealtimeProvider>
        <html lang="en">
            <ThemeProvider theme={theme}>
              <Navigation theme={theme} />
              <div className={styles.content}>
                {children}
              </div>
            </ThemeProvider>
        </html>
      </AblyRealtimeProvider>
    </AuthProvider>
  );
}

Then, I set up a channel provider and wrap a route with it.

// ably-channel.tsx
'use client';

import { ChannelProvider } from 'ably/react';

interface Props {
  channelName: string;
  children: React.ReactNode;
}
export default function Channel({ channelName, children }: Props) {
  return <ChannelProvider channelName={channelName}>{children}</ChannelProvider>;
}
import { Container } from '@radix-ui/themes';
import AiActions from 'components/ai/jobs/actions';
import Callouts from '@/components/callout/callouts';
import JobInstructions from './instructions';
import JobHeader from './header';
import { getJobData } from 'utils/jobs/get-job-data';
import RealtimeStatus from './realtime-status';
import AblyErrorReceiver from '@/components/ably/error-receiver';
import Channel from '@/components/ably/channel';

interface Props {
  params: { jobId: string; };
  searchParams: { [key: string]: string | string[] | undefined; };
}
export default async function JobPage({ params: { jobId }, searchParams }: Props) {
  const { job } = await getJobData({ jobId });

  const channelName = `realtime-status-${jobId}`;

  if (!job) {
    return null; // TODO: 404
  }

  return (
    <Container>
      <Channel channelName={channelName}>
        <JobHeader jobId={jobId} job={job} />
        <AblyErrorReceiver channelName={channelName} />
        <Callouts subscriptions={[jobId]} />
        <JobInstructions jobId={jobId} />
        <RealtimeStatus jobId={jobId} />
        <AiActions jobId={jobId} searchParams={searchParams} />
      </Channel>
    </Container>
  );
}

I then use useChannel in one of the children

'use client';

import { useChannel } from 'ably/react';
import useCallouts from '@/hooks/use-callouts';
import { ErrorMessage } from '@/utils/realtime/push-message';

interface Props {
  channelName: string;
}

export default function AblyErrorReceiver({ channelName }: Props) {
  const { setCallout } = useCallouts();

  useChannel(channelName, 'error', (message) => {
    setCallout(channelName, { type: 'error', message: (message.data as ErrorMessage)?.message });
  });

  return null;
}

Ably then proceeds to establish tons of connections. It's typically 20-40 but got over 250 one time and surpassed my free tier usage just on dev with one user (me).

It's currently unusable due to this. I've looked for what I might be doing wrong but can't figure it out.

Check out these stats of running this on dev. I asked about this on Discord with no response so decided to start an issue. I can't imagine anyone using NextJS is using Ably due to this issue.
Screenshot 2024-06-01 at 6 41 21 AM

┆Issue is synchronized with this Jira Task by Unito

@grantsingleton
Copy link
Author

I found the issue goes away when initialized like this:

'use client';

import * as Ably from 'ably';
import { AblyProvider } from 'ably/react';
import { useEffect, useRef } from 'react';

interface AblyProviderProps {
  children: React.ReactNode;
}

export default function AblyRealtimeProvider({ children }: AblyProviderProps) {
  const clientRef = useRef<Ably.Realtime>();

  useEffect(() => {
    if (!clientRef.current) {
      const client = new Ably.Realtime({ key: process.env.NEXT_PUBLIC_ABLY_CLIENT_API_KEY });
      clientRef.current = client;
    }
  }, []);

  if (!clientRef.current) return <>{children}</>

  return <AblyProvider client={clientRef.current} key='ably'>{children}</AblyProvider>;
}

Of course, as should have been obvious. const client = new Ably.Realtime({ key: process.env.NEXT_PUBLIC_ABLY_CLIENT_API_KEY }); is establishing a connection each time the app re-renders. I found that even the Next JS / Ably example does it this way and must have the same issue with too many connections.

Anyone else using Ably in a Next JS app and knows of a better way to do this than I have done here with the ref?

The problem with this method is the downstream ChannelProviders now throw errors since the ably client does not immediately exist.

@grantsingleton grantsingleton changed the title Way too many connections on dev Way too many connections on dev with Next JS Jun 2, 2024
@grantsingleton grantsingleton changed the title Way too many connections on dev with Next JS Too many Ably client connections on dev with Next JS Jun 2, 2024
@grantsingleton
Copy link
Author

grantsingleton commented Jun 2, 2024

Ive also tried this method of initializing outside of the function which seems to be what the react docs suggest:

'use client';

import * as Ably from 'ably';
import { AblyProvider } from 'ably/react';
import { useEffect, useRef } from 'react';

interface AblyProviderProps {
  children: React.ReactNode;
}

const client = new Ably.Realtime({ key: process.env.NEXT_PUBLIC_ABLY_CLIENT_API_KEY });

export default function AblyRealtimeProvider({ children }: AblyProviderProps) {
  return <AblyProvider client={client} key='ably'>{children}</AblyProvider>;
}

but this still has the issue of creating a connection every time you refresh the page. The only solution i've found that maintains one connection is the ref method shown in the previous comment but it isnt usable since it throws errors that brief instant that the client does not exist. Ably is still currently unusable in next js due to this.

Question: can the Ably.Realtime not be a singleton? Why is it creating new connections?

@ttypic
Copy link
Collaborator

ttypic commented Jun 2, 2024

Hey @grantsingleton,

Thanks for bringing this up! Sorry to hear that you're having trouble with the Next.js setup. Improving the developer experience for Next.js is on our roadmap. We'll discuss this internally and come back with recommendations for your use case.

Meanwhile I looked at solution with useRef that you shared, and problem with ChannelProviders can be easily fixed. There is no need to initialize Ably client in useEffect. Ably client constructor is very lightweight and lazy. It's better to rewrite it this way:

export default function AblyRealtimeProvider({ children }: AblyProviderProps) {
  const clientRef = useRef<Ably.Realtime>();

  if (!clientRef.current) {
      const client = new Ably.Realtime({ key: process.env.NEXT_PUBLIC_ABLY_CLIENT_API_KEY });
      clientRef.current = client;
  }

  return <AblyProvider client={clientRef.current}>{children}</AblyProvider>;
}

@grantsingleton
Copy link
Author

@ttypic thanks for looking into this. I tried your method and it still allows multiple connections. I got it over 10 on dev. Not sure how. Right now the only method able to hold it to 1 connection is putting it in a useEffect. I'll be trying other methods this week.

@AkbarBakhshi
Copy link

I have been using the method with dynamically importing the component and then using the api with token request as shown in the example below and my connection numbers are right.

https://github.com/ably-labs/NextJS-chat-app/tree/main/app

@jnovak-SM2Dev
Copy link

@ttypic Can the correct details be added to the react/next docs? It would nice if people didn't have to go through all the open issues to find a solution.

@VeskeR VeskeR added the bug Something isn't working. It's clear that this does need to be fixed. label Jun 24, 2024
@mikey555
Copy link

mikey555 commented Aug 3, 2024

I'm having this issue too and can't figure out how to reduce the number of connections. Do we know if this is or what makes this a NextJS-specific issue?

@VeskeR
Copy link
Contributor

VeskeR commented Aug 6, 2024

Hi @mikey555 !

After further investigation, we found that the issue is often caused by the hot reloading mechanism (referred to as Fast Refresh in newer Next.js versions). During development, if you update a file containing a new Ably.Realtime call, the HMR mechanism in your development environment (React, Vite, Next.js apps all include HMR) refreshes the file, creating a new client and opening a new connection. However, the previous client is unaware of this replacement, so the connection count continues to rise with each file edit. This issue is not specific to Next.js but rather applies to any development environment with HMR.
Thus, if you frequently edit a file with a new Ably.Realtime call during development, the connection count will keep increasing.

While we're working on providing a way for the library to detect when it has been reloaded and needs to close, the following workaround should help:

Simply place your new Ably.Realtime call in a separate file from those you're constantly updating. For example, extract it into a wrapper component that won't be edited frequently. If that's not feasible, you can manually refresh the page you're working on from time to time to close all open clients and connections.

@VeskeR VeskeR added enhancement New feature or improved functionality. documentation Improvements or additions to public interface documentation (API reference or readme). and removed bug Something isn't working. It's clear that this does need to be fixed. labels Aug 6, 2024
@TreNgheDiCode
Copy link

TreNgheDiCode commented Aug 19, 2024

Hi, I'm also having the same issue, what I did is put Ably in a React hook and using Singleton Pattern may decrease the connections flows.

'use client';

import * as Ably from "ably";

class AblyClient {
  private static instance: Ably.Realtime | null = null;

  private constructor() {} // Prevent direct instantiation

  public static getInstance(): Ably.Realtime | null {
    // Ensure this runs only on the client side
    if (typeof window === "undefined") {
      return null;
    }

    if (!AblyClient.instance) {
      AblyClient.instance = new Ably.Realtime({
        authUrl: "/api/ably",
        authMethod: "POST",
      });
    }
    return AblyClient.instance;
  }
}

export default AblyClient;

In the other components, just call:

 const client = AblyClient.getInstance(); 

image

Hi @mikey555 !

After further investigation, we found that the issue is often caused by the hot reloading mechanism (referred to as Fast Refresh in newer Next.js versions). During development, if you update a file containing a new Ably.Realtime call, the HMR mechanism in your development environment (React, Vite, Next.js apps all include HMR) refreshes the file, creating a new client and opening a new connection. However, the previous client is unaware of this replacement, so the connection count continues to rise with each file edit. This issue is not specific to Next.js but rather applies to any development environment with HMR. Thus, if you frequently edit a file with a new Ably.Realtime call during development, the connection count will keep increasing.

While we're working on providing a way for the library to detect when it has been reloaded and needs to close, the following workaround should help:

Simply place your new Ably.Realtime call in a separate file from those you're constantly updating. For example, extract it into a wrapper component that won't be edited frequently. If that's not feasible, you can manually refresh the page you're working on from time to time to close all open clients and connections.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to public interface documentation (API reference or readme). enhancement New feature or improved functionality.
Development

No branches or pull requests

7 participants