Skip to content

突発イベント通知システム ​

このドキュメントは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;
  }
}

実装注意事項 ​

  1. スケーラビリティ: 大量ユーザーへの効率的な配信
  2. エラー処理: 送信失敗時の適切な再試行メカニズム
  3. ユーザー体験: 通知設定の柔軟性と細かな制御
  4. 監視: 送信成功率と失敗原因の詳細ログ
  5. プライバシー: 通知内容の適切な制限と匿名化