Next.js+URQL環境でPostgreSQLのPub/Subを使ったGraphQL Subscription

[Next.js + サーバーサイドTypeScript + 関数フレーバーでクリーンなアプリを作ったので実装意図とか書く Advent Calendar 2022](https://qiita.com/advent-calendar/2022/shwld)
の**21日目**。[株式会社mofmof](https://www.mof-mof.co.jp/)に生息している[shwld](https://twitter.com/shwld)です。

前日は[Mailgunでメーラーを実装する](https://qiita.com/shwld/items/054e214dd7800ac1b1e3)について書きました
# Next.jsでGraphQL Subscription環境を構築する

## クライアントサイド (next-urql)
```ts:/apps/web/src/graphql/withGraphQLClient.tsx
withUrqlClient((_ssrExchange, _ctx) => ({
  url: `${API_HOST}/api/graphql`,
  exchanges: [
    dedupExchange,
    cache,
    fetchExchange,
    yogaExchange(),
  ],
})
```
source: [/apps/web/src/graphql/withGraphQLClient.tsx](https://github.com/shwld/uzumaki/blob/advent-calendar/apps/web/src/graphql/withGraphQLClient.tsx)

## サーバーサイド

### Pub/Subの実装

[pg-pubsub](https://github.com/voxpelli/node-pg-pubsub)を使っています。

今回PostgreSQLのPub/Subを使おうと思ったのは、インフラコスト削減が理由でした。
また、当初[GraphQL Yoga](https://the-guild.dev/graphql/yoga-server)を使っていなかったのもありました。
調べていると無料Redisも多少あるのと、途中でYogaに乗り換えたので、普通に実装するならRedisとYogaを使って実装するのが良い気がしています。

幸いPub/Subクライアントの実装をインフラ層で行っているので、乗り換えるのは簡単な構造になっています。

```ts:/infrastructures/db-pubsub/src/index.ts
import PGPubsub from 'pg-pubsub';
import type { Pubsub } from 'domain-interfaces';
import asyncify from 'callback-to-async-iterator';

const storyChannelName = (projectId: string) => `project-${projectId}-stories`;

export const createPubsubClient = (): Pubsub => {
  const pubsubInstance = new PGPubsub(process.env.DATABASE_URL);

  return {
    story: {
      subscribe({ projectId }) {
        return asyncify(async handler => {
          pubsubInstance.addChannel(storyChannelName(projectId), handler);
        });
      },
      publish(item) {
        pubsubInstance.publish(storyChannelName(item.object.projectId), item);
      },
    },
  };
};
```
source: [/infrastructures/db-pubsub/src/index.ts](https://github.com/shwld/uzumaki/blob/advent-calendar/infrastructures/db-pubsub/src/index.ts)

### Subscribe
```ts:/use-cases/graphql-resolvers/src/modules/story/subscription-resolvers/story.update/subscribe-story-update.ts#L19-L28
const subscribed = context.pubsub.story.subscribe({
  projectId: args.projectId,
});
for await (const it of subscribed) {
  if (it.triggeredBy.id === context.currentUser.id) continue;

  yield {
    subscribeStoryUpdate: it.object,
  };
}
```
source: [/use-cases/graphql-resolvers/src/modules/story/subscription-resolvers/story.update/subscribe-story-update.ts](https://github.com/shwld/uzumaki/blob/advent-calendar/use-cases/graphql-resolvers/src/modules/story/subscription-resolvers/story.update/subscribe-story-update.ts#L19-L28)

### Publish

```ts:/use-cases/graphql-resolvers/src/modules/story/mutation-resolvers/story.update/update-story.ts#L58-L60
context.pubsub.story.publish({
  object: story,
  triggeredBy: user,
})
```
source: [/use-cases/graphql-resolvers/src/modules/story/mutation-resolvers/story.update/update-story.ts#L58-L60](https://github.com/shwld/uzumaki/blob/advent-calendar/use-cases/graphql-resolvers/src/modules/story/mutation-resolvers/story.update/update-story.ts#L58-L60)
# 次回予告
明日は[バックグラウンドワーカー](https://qiita.com/shwld/items/6674288fe46eae58cdb0)について書きます。