Files
outline/server/queues/tasks/GroupMentionedInCommentNotificationsTask.ts
T
Copilot 7e2d2542b0 Split group mention notifications into separate background task (#11040)
* Initial plan

* Split group notifications into separate background task

- Created GroupMentionedInCommentNotificationsTask for handling group mention notifications
- Updated CommentCreatedNotificationsTask to delegate group notifications
- Updated CommentUpdatedNotificationsTask to delegate group notifications
- Added comprehensive tests for the new task
- Improved scalability with batch processing (10 members per batch)
- Enhanced resilience by isolating group notification failures

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Optimize database queries in GroupMentionedInCommentNotificationsTask

- Batch fetch users instead of individual queries for better performance
- Add defensive comment explaining duplicate mentions check
- Create user map for O(1) lookups within batch

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Add comment explaining concurrency control in batch processing

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Remove unnecessary document loading - use event properties directly

- Removed Document import and document loading query
- Use event.documentId and event.teamId directly
- Reduces database queries and improves performance
- canUserAccessDocument loads document internally when needed

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Fix TypeScript errors and test failures

- Fixed test: buildComment requires documentId parameter
- Fixed type error: use nullish coalescing for group.actorId ?? event.actorId
- Fixed type definition: use Omit<CommentEvent, "data"> to avoid type conflicts
- All TypeScript errors resolved

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* test

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2026-01-03 10:14:56 -05:00

85 lines
2.6 KiB
TypeScript

import { Op } from "sequelize";
import { NotificationEventType } from "@shared/types";
import { Group, GroupUser, Notification, User } from "@server/models";
import type { CommentEvent } from "@server/types";
import { canUserAccessDocument } from "@server/utils/permissions";
import { BaseTask, TaskPriority } from "./base/BaseTask";
type GroupMentionEvent = Omit<CommentEvent, "data"> & {
data: {
groupId: string;
actorId: string;
};
};
export default class GroupMentionedInCommentNotificationsTask extends BaseTask<GroupMentionEvent> {
public async perform(event: GroupMentionEvent) {
const { groupId, actorId } = event.data;
// Defensive check: ensure group has mentions enabled.
// This is also checked in the parent task, but we verify here
// for resilience in case this task is scheduled directly.
const groupModel = await Group.findByPk(groupId);
if (groupModel?.disableMentions) {
return;
}
// Process group members in batches for scalability
await GroupUser.findAllInBatches<GroupUser>(
{
where: {
groupId,
userId: {
[Op.ne]: actorId,
},
},
order: [["permission", "ASC"]],
batchLimit: 10,
},
async (groupUsers) => {
// Batch fetch all users to reduce database queries
const userIds = groupUsers.map((gu) => gu.userId);
const users = await User.findAll({
where: {
id: userIds,
},
});
// Create a map for quick user lookup
const userMap = new Map(users.map((u) => [u.id, u]));
// Process notifications for this batch (limited to 10 concurrent operations
// by the batch size to avoid overwhelming the database connection pool)
await Promise.all(
groupUsers.map(async (groupUser) => {
const recipient = userMap.get(groupUser.userId);
if (
recipient &&
recipient.subscribedToEventType(
NotificationEventType.GroupMentionedInComment
) &&
(await canUserAccessDocument(recipient, event.documentId))
) {
await Notification.create({
event: NotificationEventType.GroupMentionedInComment,
groupId,
userId: recipient.id,
actorId,
teamId: event.teamId,
documentId: event.documentId,
commentId: event.modelId,
});
}
})
);
}
);
}
public get options() {
return {
priority: TaskPriority.Background,
};
}
}