突発イベント通知システム
このドキュメントはNotionの内容を拡張したものです。詳細な実装仕様やTypeScriptコード例が追加されています。
概要
突発イベントのプッシュ通知システムの詳細仕様です。PUBLICとFRIEND_ONLYイベントの送信対象条件、1分ごとのCronJob実行、最大10人/分の送信制限、通知文テンプレートについて定義します。
通知システム設定
基本パラメーター
typescript
interface EventNotificationConfig {
cronInterval: string; // Cronジョブ実行間隔
maxNotificationsPerMinute: number; // 1分間の最大送信数
notificationDelay: number; // イベント作成からの最小待機時間(分)
retryAttempts: number; // 送信失敗時の再試行回数
retryInterval: number; // 再試行間隔(秒)
}
// デフォルト設定
const DEFAULT_CONFIG: EventNotificationConfig = {
cronInterval: '* * * * *', // 毎分実行
maxNotificationsPerMinute: 10, // 最大10件/分
notificationDelay: 0, // 即座に送信
retryAttempts: 3, // 3回まで再試行
retryInterval: 30 // 30秒間隔
};イベントタイプ別送信条件
typescript
enum EventType {
PUBLIC = 'PUBLIC',
FRIEND_ONLY = 'FRIEND_ONLY',
PRIVATE = 'PRIVATE'
}
interface EventNotificationRule {
eventType: EventType;
sendToAll: boolean;
targetUserConditions: UserCondition[];
excludeConditions: ExcludeCondition[];
}
const NOTIFICATION_RULES: EventNotificationRule[] = [
// PUBLIC イベント
{
eventType: EventType.PUBLIC,
sendToAll: true,
targetUserConditions: [
{
type: 'active_user',
lastLoginWithin: 30 // 30日以内にログインしたユーザー
},
{
type: 'notification_enabled',
settingKey: 'push_event_notifications'
}
],
excludeConditions: [
{
type: 'blocked_user',
relationTo: 'event_creator'
},
{
type: 'restricted_user',
restrictionLevel: 'severe'
}
]
},
// FRIEND_ONLY イベント
{
eventType: EventType.FRIEND_ONLY,
sendToAll: false,
targetUserConditions: [
{
type: 'friend_of_creator'
},
{
type: 'notification_enabled',
settingKey: 'push_friend_notifications'
}
],
excludeConditions: [
{
type: 'blocked_user',
relationTo: 'event_creator'
}
]
}
];CronJob実装
スケジュール実行ロジック
typescript
interface NotificationJob {
id: string;
eventId: string;
createdAt: Date;
scheduledAt: Date;
status: 'pending' | 'processing' | 'completed' | 'failed';
targetUserIds: string[];
sentCount: number;
failedCount: number;
lastAttempt: Date | null;
attempts: number;
}
class EventNotificationScheduler {
private readonly config: EventNotificationConfig;
private readonly notificationQueue: NotificationJob[] = [];
private isProcessing = false;
constructor(config: EventNotificationConfig) {
this.config = config;
this.startCronJob();
}
private startCronJob(): void {
// 毎分実行
setInterval(async () => {
if (!this.isProcessing) {
await this.processNotificationQueue();
}
}, 60 * 1000); // 1分間隔
}
private async processNotificationQueue(): Promise<void> {
this.isProcessing = true;
try {
// 送信対象のジョブを取得(最大10件)
const pendingJobs = await this.getPendingJobs(
this.config.maxNotificationsPerMinute
);
for (const job of pendingJobs) {
await this.processNotificationJob(job);
}
} catch (error) {
console.error('Notification processing error:', error);
} finally {
this.isProcessing = false;
}
}
private async getPendingJobs(limit: number): Promise<NotificationJob[]> {
const now = new Date();
return this.notificationQueue
.filter(job =>
job.status === 'pending' &&
job.scheduledAt <= now &&
job.attempts < this.config.retryAttempts
)
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime())
.slice(0, limit);
}
}送信対象ユーザー選定
typescript
interface UserSelectionService {
getTargetUsersForEvent(event: Event): Promise<string[]>;
}
class EventNotificationTargetSelector implements UserSelectionService {
async getTargetUsersForEvent(event: Event): Promise<string[]> {
const rule = NOTIFICATION_RULES.find(r => r.eventType === event.type);
if (!rule) return [];
let targetUsers: string[] = [];
if (rule.sendToAll) {
// 全ユーザー対象(PUBLIC イベント)
targetUsers = await this.getAllActiveUsers();
} else {
// 特定ユーザー対象(FRIEND_ONLY イベント)
targetUsers = await this.getFriendsOfUser(event.creatorId);
}
// 条件フィルタリングの適用
targetUsers = await this.applyUserConditions(
targetUsers,
rule.targetUserConditions,
event
);
// 除外条件の適用
targetUsers = await this.applyExcludeConditions(
targetUsers,
rule.excludeConditions,
event
);
return targetUsers;
}
private async getAllActiveUsers(): Promise<string[]> {
// 30日以内にログインしたアクティブユーザーを取得
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
return await this.userRepository.findActiveUserIds({
lastLoginAfter: thirtyDaysAgo,
isActive: true
});
}
private async getFriendsOfUser(userId: string): Promise<string[]> {
return await this.friendRepository.getFriendIds(userId);
}
private async applyUserConditions(
userIds: string[],
conditions: UserCondition[],
event: Event
): Promise<string[]> {
let filteredIds = userIds;
for (const condition of conditions) {
switch (condition.type) {
case 'notification_enabled':
filteredIds = await this.filterByNotificationSetting(
filteredIds,
condition.settingKey
);
break;
case 'active_user':
filteredIds = await this.filterByLastLogin(
filteredIds,
condition.lastLoginWithin
);
break;
case 'friend_of_creator':
filteredIds = await this.filterByFriendship(
filteredIds,
event.creatorId
);
break;
}
}
return filteredIds;
}
private async applyExcludeConditions(
userIds: string[],
conditions: ExcludeCondition[],
event: Event
): Promise<string[]> {
let filteredIds = userIds;
for (const condition of conditions) {
switch (condition.type) {
case 'blocked_user':
filteredIds = await this.excludeBlockedUsers(
filteredIds,
event.creatorId
);
break;
case 'restricted_user':
filteredIds = await this.excludeRestrictedUsers(
filteredIds,
condition.restrictionLevel
);
break;
}
}
return filteredIds;
}
}通知文テンプレート
テンプレート定義
typescript
interface NotificationTemplate {
type: 'event_created';
eventType: EventType;
language: 'ja' | 'en' | 'ko' | 'zh-CN' | 'zh-TW';
title: string;
body: string;
variables: string[];
}
const NOTIFICATION_TEMPLATES: NotificationTemplate[] = [
// PUBLIC イベント - 日本語
{
type: 'event_created',
eventType: EventType.PUBLIC,
language: 'ja',
title: '新しいイベントが開催されます!',
body: '{{creatorName}}さんが「{{eventTitle}}」を開催します。今すぐ参加しよう!',
variables: ['creatorName', 'eventTitle']
},
// FRIEND_ONLY イベント - 日本語
{
type: 'event_created',
eventType: EventType.FRIEND_ONLY,
language: 'ja',
title: 'フレンドがイベントを開催中!',
body: 'フレンドの{{creatorName}}さんが「{{eventTitle}}」を開催しています。参加しませんか?',
variables: ['creatorName', 'eventTitle']
},
// PUBLIC イベント - 英語
{
type: 'event_created',
eventType: EventType.PUBLIC,
language: 'en',
title: 'New Event Available!',
body: '{{creatorName}} created "{{eventTitle}}". Join now!',
variables: ['creatorName', 'eventTitle']
},
// FRIEND_ONLY イベント - 英語
{
type: 'event_created',
eventType: EventType.FRIEND_ONLY,
language: 'en',
title: 'Your friend created an event!',
body: 'Your friend {{creatorName}} created "{{eventTitle}}". Want to join?',
variables: ['creatorName', 'eventTitle']
}
];テンプレート処理
typescript
interface NotificationData {
userId: string;
eventId: string;
creatorName: string;
eventTitle: string;
eventType: EventType;
userLanguage: string;
}
class NotificationTemplateProcessor {
generateNotification(data: NotificationData): {title: string, body: string} {
const template = this.findTemplate(
data.eventType,
data.userLanguage
);
if (!template) {
// フォールバック:日本語テンプレート
return this.generateNotification({
...data,
userLanguage: 'ja'
});
}
return {
title: this.processTemplate(template.title, data),
body: this.processTemplate(template.body, data)
};
}
private findTemplate(
eventType: EventType,
language: string
): NotificationTemplate | null {
return NOTIFICATION_TEMPLATES.find(t =>
t.eventType === eventType &&
t.language === language
) || null;
}
private processTemplate(
template: string,
data: NotificationData
): string {
return template
.replace('{{creatorName}}', data.creatorName)
.replace('{{eventTitle}}', data.eventTitle);
}
}送信制限とレート制御
送信レート制御
typescript
interface RateLimiter {
canSend(): boolean;
recordSent(): void;
reset(): void;
}
class NotificationRateLimiter implements RateLimiter {
private sentCount = 0;
private lastReset = new Date();
private readonly maxPerMinute: number;
constructor(maxPerMinute: number) {
this.maxPerMinute = maxPerMinute;
}
canSend(): boolean {
this.checkReset();
return this.sentCount < this.maxPerMinute;
}
recordSent(): void {
this.sentCount++;
}
reset(): void {
this.sentCount = 0;
this.lastReset = new Date();
}
private checkReset(): void {
const now = new Date();
const minutesSinceReset = Math.floor(
(now.getTime() - this.lastReset.getTime()) / (60 * 1000)
);
if (minutesSinceReset >= 1) {
this.reset();
}
}
}優先度付きキュー
typescript
enum NotificationPriority {
HIGH = 1, // フレンドイベント
MEDIUM = 2, // パブリックイベント(アクティブユーザー)
LOW = 3 // パブリックイベント(非アクティブユーザー)
}
interface PriorityNotificationJob extends NotificationJob {
priority: NotificationPriority;
}
class PriorityNotificationQueue {
private queues: Map<NotificationPriority, PriorityNotificationJob[]> = new Map();
constructor() {
// 優先度別キューの初期化
Object.values(NotificationPriority).forEach(priority => {
if (typeof priority === 'number') {
this.queues.set(priority, []);
}
});
}
enqueue(job: PriorityNotificationJob): void {
const queue = this.queues.get(job.priority);
if (queue) {
queue.push(job);
// スケジュール時間順でソート
queue.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
}
}
dequeue(maxCount: number): PriorityNotificationJob[] {
const result: PriorityNotificationJob[] = [];
// 優先度順に処理
for (const priority of [
NotificationPriority.HIGH,
NotificationPriority.MEDIUM,
NotificationPriority.LOW
]) {
const queue = this.queues.get(priority);
if (!queue || result.length >= maxCount) continue;
const available = maxCount - result.length;
const jobs = queue.splice(0, available);
result.push(...jobs);
}
return result;
}
}実装注意事項
- スケーラビリティ: 大量ユーザーへの効率的な配信
- エラー処理: 送信失敗時の適切な再試行メカニズム
- ユーザー体験: 通知設定の柔軟性と細かな制御
- 監視: 送信成功率と失敗原因の詳細ログ
- プライバシー: 通知内容の適切な制限と匿名化