Skip to content

Commit e0b57b1

Browse files
authored
Merge pull request #101 from techdiary-dev/kingrayhan/socket
Kingrayhan/socket
2 parents bec220f + 46fb559 commit e0b57b1

7 files changed

Lines changed: 73 additions & 37 deletions

File tree

src/backend/services/comment.action.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import { and, eq, inArray } from "sqlkit";
1010
import { CommentPresentation } from "../models/domain-models";
1111
import { inngest } from "@/lib/inngest";
1212
import { assertCommentResourceExists } from "./notifications.payload";
13-
import { publishMessage } from "@/lib/pusher/pusher.server";
13+
import {
14+
publishMessage,
15+
REALTIME_PUSHER_EVENTS,
16+
} from "@/lib/pusher/pusher.server";
1417

1518
const sql = String.raw;
1619

@@ -71,7 +74,7 @@ export const createMyComment = async (
7174

7275
void publishMessage(
7376
`resource.${resource_type}.${resource_id}`,
74-
"comment.created",
77+
REALTIME_PUSHER_EVENTS.COMMENT_CREATED,
7578
{ scope: "comments" },
7679
);
7780

@@ -106,7 +109,7 @@ export const updateMyComment = async (
106109

107110
void publishMessage(
108111
`resource.${existing.resource_type}.${existing.resource_id}`,
109-
"comment.updated",
112+
REALTIME_PUSHER_EVENTS.COMMENT_UPDATED,
110113
{ scope: "comments" },
111114
);
112115

@@ -164,7 +167,7 @@ export const deleteMyComment = async (
164167

165168
void publishMessage(
166169
`resource.${root.resource_type}.${root.resource_id}`,
167-
"comment.deleted",
170+
REALTIME_PUSHER_EVENTS.COMMENT_DELETED,
168171
{ scope: "comments" },
169172
);
170173

src/components/comment-section.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ import { Button } from "./ui/button";
3838
import { Skeleton } from "./ui/skeleton";
3939
import { Textarea } from "./ui/textarea";
4040
import getFileUrl from "@/utils/getFileUrl";
41-
import { listenChannel } from "@/lib/pusher/pusher.client";
41+
import {
42+
listenChannel,
43+
REALTIME_PUSHER_EVENTS,
44+
} from "@/lib/pusher/pusher.client";
4245

4346
const Context = React.createContext<
4447
{ mutatingId?: string; setMutatingId: (id?: string) => void } | undefined
@@ -166,9 +169,9 @@ export const CommentSection = (props: {
166169
});
167170
};
168171
return listenChannel(channelName, {
169-
"comment.created": invalidate,
170-
"comment.updated": invalidate,
171-
"comment.deleted": invalidate,
172+
[REALTIME_PUSHER_EVENTS.COMMENT_CREATED]: invalidate,
173+
[REALTIME_PUSHER_EVENTS.COMMENT_UPDATED]: invalidate,
174+
[REALTIME_PUSHER_EVENTS.COMMENT_DELETED]: invalidate,
172175
});
173176
}, [props.resource_id, props.resource_type, queryClient]);
174177

src/components/providers/RealtimeProvider.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
"use client";
22

3-
import { listenChannel } from "@/lib/pusher/pusher.client";
3+
import {
4+
listenChannel,
5+
REALTIME_PUSHER_EVENTS,
6+
} from "@/lib/pusher/pusher.client";
47
import { useSession } from "@/store/session.atom";
58
import { useQueryClient } from "@tanstack/react-query";
69
import React, { PropsWithChildren, useEffect } from "react";
@@ -28,11 +31,14 @@ export function RealtimeProvider({ children }: PropsWithChildren) {
2831
if (!userId) return;
2932

3033
const channelName = `private-user.${userId}`;
31-
return listenChannel(channelName, "notification.new", () => {
34+
const invalidate = () => {
3235
queryClient.invalidateQueries({ queryKey: ["my-notifications"] });
3336
queryClient.invalidateQueries({
3437
queryKey: ["unread-notification-count"],
3538
});
39+
};
40+
return listenChannel(channelName, {
41+
[REALTIME_PUSHER_EVENTS.NOTIFICATION_NEW]: invalidate,
3642
});
3743
}, [userId, queryClient]);
3844

src/lib/inngest.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import {
77
import { persistenceRepository } from "@/backend/persistence/persistence-repositories";
88
import { ActionException } from "@/backend/services/RepositoryException";
99
import { buildPersistableNotification } from "@/backend/services/notifications.payload";
10-
import { publishMessage } from "@/lib/pusher/pusher.server";
10+
import {
11+
publishMessage,
12+
REALTIME_PUSHER_EVENTS,
13+
} from "@/lib/pusher/pusher.server";
1114
import { deleteExpiredArticles } from "@/backend/services/article-cleanup-service";
1215

1316
const notificationPayloadSchema = z.object({
@@ -191,9 +194,11 @@ export const persistNotificationFn = inngest.createFunction(
191194

192195
// Broadcast a lightweight signal so the recipient's browser can invalidate
193196
// its TanStack Query caches without polling.
194-
await publishMessage(`private-user.${data.recipient_id}`, "notification.new", {
195-
scope: "notifications",
196-
});
197+
await publishMessage(
198+
`private-user.${data.recipient_id}`,
199+
REALTIME_PUSHER_EVENTS.NOTIFICATION_NEW,
200+
{ scope: "notifications" },
201+
);
197202

198203
return { success: true };
199204
},

src/lib/pusher/pusher.client.ts

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import Pusher from "pusher-js";
22
import { env } from "@/env";
3+
import type {
4+
RealtimeListenHandlers,
5+
RealtimePusherEvent,
6+
} from "./realtime-events";
7+
8+
export {
9+
REALTIME_PUSHER_EVENTS,
10+
type RealtimeListenHandlers,
11+
type RealtimePusherEvent,
12+
} from "./realtime-events";
313

414
let _pusherClient: Pusher | null = null;
515

@@ -23,40 +33,25 @@ function getPusherClient(): Pusher | null {
2333
return _pusherClient;
2434
}
2535

26-
type EventHandlers = Record<string, () => void>;
27-
28-
export function listenChannel(
29-
channel: string,
30-
handlers: EventHandlers,
31-
): () => void;
3236
export function listenChannel(
3337
channel: string,
34-
event: string,
35-
handler: () => void,
36-
): () => void;
37-
export function listenChannel(
38-
channel: string,
39-
eventOrHandlers: string | EventHandlers,
40-
handler?: () => void,
38+
handlers: RealtimeListenHandlers,
4139
): () => void {
42-
const handlers: EventHandlers =
43-
typeof eventOrHandlers === "string" && handler !== undefined
44-
? { [eventOrHandlers]: handler }
45-
: (eventOrHandlers as EventHandlers);
46-
4740
const pusher = getPusherClient();
4841
if (!pusher) {
4942
return () => {};
5043
}
5144

5245
const ch = pusher.subscribe(channel);
53-
for (const [event, fn] of Object.entries(handlers)) {
54-
ch.bind(event, fn);
46+
for (const event of Object.keys(handlers) as RealtimePusherEvent[]) {
47+
const fn = handlers[event];
48+
if (fn) ch.bind(event, fn);
5549
}
5650

5751
return () => {
58-
for (const [event, fn] of Object.entries(handlers)) {
59-
ch.unbind(event, fn);
52+
for (const event of Object.keys(handlers) as RealtimePusherEvent[]) {
53+
const fn = handlers[event];
54+
if (fn) ch.unbind(event, fn);
6055
}
6156
pusher.unsubscribe(channel);
6257
};

src/lib/pusher/pusher.server.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import Pusher from "pusher";
22
import { env } from "@/env";
3+
import type { RealtimePusherEvent } from "./realtime-events";
4+
5+
export {
6+
REALTIME_PUSHER_EVENTS,
7+
type RealtimePusherEvent,
8+
} from "./realtime-events";
39

410
/**
511
* Lazy singleton for the server-side Pusher/Soketi client.
@@ -28,7 +34,7 @@ export const pusherServer = createPusherServer();
2834
*/
2935
export async function publishMessage(
3036
channel: string,
31-
event: "comment.created" | "comment.updated" | "comment.deleted",
37+
event: RealtimePusherEvent,
3238
data: Record<string, unknown> = {},
3339
): Promise<void> {
3440
console.log(`

src/lib/pusher/realtime-events.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Canonical Pusher event names for app realtime (invalidate / refetch signals).
3+
* Extend here so client listeners and server triggers stay aligned.
4+
*/
5+
export const REALTIME_PUSHER_EVENTS = {
6+
NOTIFICATION_NEW: "notification.new",
7+
COMMENT_CREATED: "comment.created",
8+
COMMENT_UPDATED: "comment.updated",
9+
COMMENT_DELETED: "comment.deleted",
10+
} as const;
11+
12+
export type RealtimePusherEvent =
13+
(typeof REALTIME_PUSHER_EVENTS)[keyof typeof REALTIME_PUSHER_EVENTS];
14+
15+
/** Handlers object for `listenChannel`: only known event names, each optional. */
16+
export type RealtimeListenHandlers = {
17+
[K in RealtimePusherEvent]?: () => void;
18+
};

0 commit comments

Comments
 (0)