Invalidate notification cache when deleting a status

pull/7241/head
Your Name 1 week ago
parent e0550c441c
commit f1e4536d19

@ -172,9 +172,17 @@ class RemoteStatusDelete implements ShouldBeUniqueUntilProcessing, ShouldQueue
MediaTag::whereIn('id', $mediaTagIds)->delete(); MediaTag::whereIn('id', $mediaTagIds)->delete();
} }
Mention::whereStatusId($status->id)->forceDelete(); Mention::whereStatusId($status->id)->forceDelete();
Notification::whereItemType(Status::class) // Per-row (not bulk) so NotificationObserver::forceDeleted fires and
->whereItemId($status->id) // NotificationService::del invalidates the 24h cached ITEM_KEY snapshot;
->forceDelete(); // a bulk forceDelete() would leave the web feed serving the deleted
// status as a ghost. Match the legacy 'App\Status' morph alias too.
Notification::whereIn('item_type', ['App\Status', Status::class])
->where('item_id', $status->id)
->cursor()
->each(function ($not) {
NotificationService::del($not->profile_id, $not->id);
$not->forceDeleteQuietly();
});
Report::whereObjectType(Status::class) Report::whereObjectType(Status::class)
->whereObjectId($status->id) ->whereObjectId($status->id)
->delete(); ->delete();

@ -158,9 +158,17 @@ class StatusDelete implements ShouldQueue
} }
Mention::whereStatusId($status->id)->forceDelete(); Mention::whereStatusId($status->id)->forceDelete();
Notification::whereItemType(Status::class) // Per-row (not bulk) so NotificationObserver::forceDeleted fires and
->whereItemId($status->id) // NotificationService::del invalidates the 24h cached ITEM_KEY snapshot;
->forceDelete(); // a bulk forceDelete() would leave the web feed serving the deleted
// status as a ghost. Match the legacy 'App\Status' morph alias too.
Notification::whereIn('item_type', ['App\Status', Status::class])
->where('item_id', $status->id)
->cursor()
->each(function ($not) {
NotificationService::del($not->profile_id, $not->id);
$not->forceDeleteQuietly();
});
Report::whereObjectType(Status::class) Report::whereObjectType(Status::class)
->whereObjectId($status->id) ->whereObjectId($status->id)

@ -0,0 +1,84 @@
<?php
use App\Jobs\StatusPipeline\StatusDelete;
use App\Models\Notification;
use App\Models\Status;
use App\Models\User;
use App\Services\NotificationService;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Support\Facades\Cache;
uses(LazilyRefreshDatabase::class);
/*
|--------------------------------------------------------------------------
| StatusDelete invalidates cached status notifications
|--------------------------------------------------------------------------
|
| Notifications cache a fully-transformed snapshot (with an embedded status
| payload) under ITEM_KEY for 24h, and the web feed serves that verbatim. When
| a status is deleted, its status-typed notifications must be removed per-row so
| NotificationObserver::forceDeleted -> NotificationService::del invalidates the
| cached snapshot. A bulk forceDelete() fires no model events and would leave
| the web feed serving the deleted status as a ghost for up to 24h.
|
*/
/**
* Persist a status-typed like notification and warm its ITEM_KEY snapshot, the
* way createNotification/setNotification does on the live path.
*/
function warmStatusNotification(int $profileId, int $actorId, int $statusId, string $itemType): Notification
{
$n = new Notification;
$n->profile_id = $profileId;
$n->actor_id = $actorId;
$n->action = 'like';
$n->item_id = $statusId;
$n->item_type = $itemType;
$n->save();
NotificationService::setNotification($n); // writes ITEM_KEY
NotificationService::set($n->profile_id, $n->id);
return $n;
}
it('invalidates the cached notification snapshot when its status is deleted', function () {
$user = User::factory()->create();
$user->refresh();
$actor = User::factory()->create();
$actor->refresh();
$status = Status::factory()->create(['profile_id' => $user->profile_id, 'scope' => 'public']);
$n = warmStatusNotification($user->profile_id, $actor->profile_id, $status->id, Status::class);
// Snapshot is warm before deletion.
expect(Cache::get(NotificationService::ITEM_KEY.$n->id))->not->toBeNull();
(new StatusDelete($status))->unlinkRemoveMedia($status);
// Row is force-deleted and the cached snapshot was invalidated by the
// observer (no manual NotificationService::del in the test).
expect(Notification::withTrashed()->whereId($n->id)->exists())->toBeFalse();
expect(Cache::get(NotificationService::ITEM_KEY.$n->id))->toBeNull();
});
it('invalidates and removes legacy App\\Status notifications too', function () {
$user = User::factory()->create();
$user->refresh();
$actor = User::factory()->create();
$actor->refresh();
$status = Status::factory()->create(['profile_id' => $user->profile_id, 'scope' => 'public']);
// Legacy morph alias predating the App\ -> App\Models\ migration.
$n = warmStatusNotification($user->profile_id, $actor->profile_id, $status->id, 'App\Status');
expect(Cache::get(NotificationService::ITEM_KEY.$n->id))->not->toBeNull();
(new StatusDelete($status))->unlinkRemoveMedia($status);
// The alias-aware cleanup matches legacy rows: no DB orphan, cache cleared.
expect(Notification::withTrashed()->whereId($n->id)->exists())->toBeFalse();
expect(Cache::get(NotificationService::ITEM_KEY.$n->id))->toBeNull();
});
Loading…
Cancel
Save