whereIn('state', EventState::nonTerminalValues()) ->with('user') ->chunkById(200, fn (Collection $events) => $events->each(fn (Event $e) => $this->process($e))); } private function process(Event $event): void { $user = $event->user; $now = $user->now(); $state = $this->states->resolve($event, $user, $now); if ($state !== $event->state) { $event->state = $state; $event->save(); } if (! $state->isActive() || $now->lt($event->quiet_until)) { return; } $window = $this->windows->current($state, $user, $this->due->resolve($event, $user), $now); if ($window === null) { return; } if (Delivery::where('event_id', $event->id)->where('window_start', $window->start->utc())->exists()) { return; } $channels = $user->channels()->where('enabled', true)->get(); if ($channels->isEmpty()) { return; } $present = $channels->filter(fn (Channel $c) => $this->presence($c, $user) === Presence::Present); if ($present->isNotEmpty()) { $this->dispatch($event, $present, $window); return; } if ($window->fallbackAllowed && $now->gte($window->fallbackAt)) { $this->dispatch($event, $channels, $window); } } private function presence(Channel $channel, User $user): Presence { return $this->cache->remember( "presence:{$channel->id}", $this->presenceTtl, fn () => $this->registry->for($channel->type)->presence($user, $channel->config), ); } /** Строка pending — в транзакции ДО постановки job-а: следующий тик её уже видит. */ private function dispatch(Event $event, Collection $channels, Window $window): void { DB::transaction(function () use ($event, $channels, $window) { foreach ($channels as $channel) { $delivery = Delivery::create([ 'event_id' => $event->id, 'channel_id' => $channel->id, 'window_start' => $window->start->utc(), 'action_token' => Str::random(48), 'result' => DeliveryResult::Pending, ]); DeliverJob::dispatch($delivery->id)->afterCommit(); } }); } }