Files
dataroom/lib/trigger/dataroom-change-notification.ts
zeekayandHanzo Dev 6b68c9299e kv: one name for the store, and it is Hanzo KV
The client was already @hanzo/kv; only the vocabulary was still Redis. Every
module, symbol and comment now says what it connects to:

  lib/redis.ts                    -> lib/kv.ts                (exports kv, lockKv)
  lib/redis-job-store.ts          -> lib/kv-job-store.ts      (KvJobStore)
  lib/redis-download-job-store.ts -> lib/kv-download-job-store.ts
  lib/files/tus-redis-locker.ts   -> lib/files/tus-kv-locker.ts (KvLocker, KvLock)
  lib/api/domains/redis.ts        -> lib/api/domains/kv.ts    (getKey)
  lib/redis/                      -> lib/kv-queue/

KV_URL is now the only endpoint variable — the REDIS_URL fallback and the
redis://localhost default are gone, and the default scheme is kv://, which
@hanzo/kv parses natively (utils/index.js). .env.example gained the KV_URL it
never documented, and lost UPSTASH_REDIS_REST_LOCKER_URL/TOKEN: nothing has read
those since the client changed — the locker opens a second connection to the
same KV, so there is no second endpoint to configure.

The second connection is `lockKv`, not `locker`: both tus routes already have a
local `const locker = new KvLocker(...)`, so naming the client `locker` made the
lock take itself as its own client.

Verified against the repo's own gate (npx tsc --noEmit): 100 errors before and
after, 0 introduced, compared set-wise by file:line:code. Behaviour is untouched
— this is naming, one connection string, and dead config.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-09 10:09:34 -07:00

190 lines
5.1 KiB
TypeScript

import { logger, task } from "@trigger.dev/sdk/v3";
import prisma from "@/lib/prisma";
import { queueNotification } from "@/lib/kv-queue/dataroom-notification-queue";
import { ZViewerNotificationPreferencesSchema } from "@/lib/zod/schemas/notifications";
type NotificationPayload = {
dataroomId: string;
dataroomDocumentId: string;
senderUserId: string;
teamId: string;
};
export const sendDataroomChangeNotificationTask = task({
id: "send-dataroom-change-notification",
retry: { maxAttempts: 3 },
run: async (payload: NotificationPayload) => {
const viewers = await prisma.viewer.findMany({
where: {
teamId: payload.teamId,
views: {
some: {
dataroomId: payload.dataroomId,
viewType: "DATAROOM_VIEW",
verified: true,
},
},
},
select: {
id: true,
notificationPreferences: true,
views: {
where: {
dataroomId: payload.dataroomId,
viewType: "DATAROOM_VIEW",
verified: true,
},
orderBy: {
viewedAt: "desc",
},
take: 1,
include: {
link: {
select: {
id: true,
slug: true,
domainSlug: true,
domainId: true,
isArchived: true,
expiresAt: true,
},
},
},
},
},
});
if (!viewers || viewers.length === 0) {
logger.info("No verified viewers found for this dataroom", {
dataroomId: payload.dataroomId,
});
return;
}
const viewersWithLinks = viewers
.map((viewer) => {
const view = viewer.views[0];
const link = view?.link;
if (
!link ||
link.isArchived ||
(link.expiresAt && new Date(link.expiresAt) < new Date())
) {
return null;
}
const parsedPreferences =
ZViewerNotificationPreferencesSchema.safeParse(
viewer.notificationPreferences,
);
if (
parsedPreferences.success &&
parsedPreferences.data.dataroom[payload.dataroomId]?.enabled === false
) {
return null;
}
const frequency =
parsedPreferences.success
? (parsedPreferences.data.dataroom[payload.dataroomId]?.frequency ??
"instant")
: "instant";
let linkUrl = "";
if (link.domainId && link.domainSlug && link.slug) {
linkUrl = `https://${link.domainSlug}/${link.slug}`;
} else {
linkUrl = `${process.env.NEXT_PUBLIC_MARKETING_URL}/view/${link.id}`;
}
return {
id: viewer.id,
linkUrl,
frequency,
};
})
.filter(
(
viewer,
): viewer is {
id: string;
linkUrl: string;
frequency: "instant" | "daily" | "weekly";
} => viewer !== null,
);
logger.info("Processed viewer links", {
viewerCount: viewersWithLinks.length,
});
for (const viewer of viewersWithLinks) {
try {
if (viewer.frequency === "daily" || viewer.frequency === "weekly") {
await queueNotification({
frequency: viewer.frequency,
viewerId: viewer.id,
dataroomId: payload.dataroomId,
teamId: payload.teamId,
dataroomDocumentId: payload.dataroomDocumentId,
senderUserId: payload.senderUserId,
});
logger.info("Queued notification for digest", {
viewerId: viewer.id,
frequency: viewer.frequency,
});
continue;
}
const response = await fetch(
`${process.env.NEXT_PUBLIC_BASE_URL}/api/jobs/send-dataroom-new-document-notification`,
{
method: "POST",
body: JSON.stringify({
dataroomId: payload.dataroomId,
linkUrl: viewer.linkUrl,
dataroomDocumentId: payload.dataroomDocumentId,
viewerId: viewer.id,
senderUserId: payload.senderUserId,
teamId: payload.teamId,
}),
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.INTERNAL_API_KEY}`,
},
},
);
if (!response.ok) {
logger.error("Failed to send dataroom notification", {
viewerId: viewer.id,
dataroomId: payload.dataroomId,
error: await response.text(),
});
continue;
}
const { message } = (await response.json()) as { message: string };
logger.info("Notification sent successfully", {
viewerId: viewer.id,
message,
});
} catch (error) {
logger.error("Error sending notification", {
viewerId: viewer.id,
error,
});
}
}
logger.info("Completed sending notifications", {
dataroomId: payload.dataroomId,
viewerCount: viewers.length,
});
return;
},
});