strip session-added comments
This commit is contained in:
@@ -148,22 +148,14 @@ pub enum DomainEvent {
|
||||
pub trait AckHandle: Send + Sync {
|
||||
async fn ack(&self) -> Result<(), DomainError>;
|
||||
async fn nack(&self) -> Result<(), DomainError>;
|
||||
/// Redeliver after `delay` without counting a failed attempt: for a
|
||||
/// delivery that was not tried (it is blocked behind a live claim), so it
|
||||
/// must not burn the retry budget that `nack` spends. A broker may still
|
||||
/// count the redelivery itself: JetStream does, toward `max_deliver`.
|
||||
async fn defer(&self, delay: std::time::Duration) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
/// Wait before retrying an event whose `attempt`-th delivery just failed:
|
||||
/// 5s doubling per attempt, capped at 5 minutes. Every backend (SQLite queue,
|
||||
/// JetStream nak, outbox relay) uses this one schedule.
|
||||
pub fn retry_backoff(attempt: u32) -> std::time::Duration {
|
||||
let secs = 5u64 * (1u64 << attempt.min(6));
|
||||
std::time::Duration::from_secs(secs.min(300))
|
||||
}
|
||||
|
||||
/// Stable identity of one queued event; the key for de-duplicating deliveries.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct EventId(uuid::Uuid);
|
||||
|
||||
@@ -194,7 +186,6 @@ pub struct EventEnvelope {
|
||||
}
|
||||
|
||||
impl EventEnvelope {
|
||||
/// `id` is `None` only for events queued before identities existed.
|
||||
pub fn new(id: Option<EventId>, event: DomainEvent, ack: Box<dyn AckHandle>) -> Self {
|
||||
Self { event, id, ack }
|
||||
}
|
||||
|
||||
@@ -2,41 +2,21 @@ use async_trait::async_trait;
|
||||
|
||||
use crate::{errors::DomainError, value_objects::UserId};
|
||||
|
||||
/// Tells the fediverse that a local account is gone (an ActivityPub `Delete`
|
||||
/// to followers). Federation-off builds wire a no-op.
|
||||
///
|
||||
/// Delivery is asynchronous: the implementation may only enqueue deliveries.
|
||||
/// Those are signed later with the actor's keypair and the tombstoned user row,
|
||||
/// which is why the purge keeps both until the tombstone cleanup.
|
||||
#[async_trait]
|
||||
pub trait FederationDeletionNotifier: Send + Sync {
|
||||
async fn notify_deleted(&self, user: &UserId) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
/// Removes everything a deleted account owns, in one transaction, and strips
|
||||
/// the user row down to a data-free tombstone.
|
||||
///
|
||||
/// What stays: the username (retired for good), role, `deleted_at`, the
|
||||
/// `purged_at` stamp and the signing keypair, so queued federation deliveries
|
||||
/// can still be signed. `purge_expired_tombstones` removes those later.
|
||||
#[async_trait]
|
||||
pub trait AccountPurger: Send + Sync {
|
||||
/// Whether the account's data has already been purged.
|
||||
async fn is_purged(&self, user: &UserId) -> Result<bool, DomainError>;
|
||||
|
||||
/// Object-storage keys the account owns (avatar, banner). Still readable
|
||||
/// after `purge`, until `forget_images`.
|
||||
async fn owned_image_keys(&self, user: &UserId) -> Result<Vec<String>, DomainError>;
|
||||
|
||||
/// One transaction; every delete is "if present", so a re-run is a no-op.
|
||||
/// Frees the email. Does nothing for an account that is gone or purged.
|
||||
async fn purge(&self, user: &UserId) -> Result<(), DomainError>;
|
||||
|
||||
/// Drops the image references once their objects are deleted.
|
||||
async fn forget_images(&self, user: &UserId) -> Result<(), DomainError>;
|
||||
|
||||
/// Deletes tombstones (user row and keypair) purged before `cutoff`.
|
||||
/// Returns how many were removed.
|
||||
async fn purge_expired_tombstones(
|
||||
&self,
|
||||
cutoff: chrono::NaiveDateTime,
|
||||
|
||||
@@ -14,8 +14,6 @@ pub trait AuthService: Send + Sync {
|
||||
|
||||
#[async_trait]
|
||||
pub trait UserRepository: Send + Sync {
|
||||
// The finders and `list_with_stats` serve live users only: tombstoned
|
||||
// (deleted) accounts are invisible to them.
|
||||
async fn find_by_email(&self, email: &Email) -> Result<Option<User>, DomainError>;
|
||||
async fn find_by_username(&self, username: &Username) -> Result<Option<User>, DomainError>;
|
||||
async fn save(&self, user: &User) -> Result<(), DomainError>;
|
||||
@@ -27,17 +25,12 @@ pub trait UserRepository: Send + Sync {
|
||||
profile: &crate::models::UserProfile,
|
||||
events: &[crate::events::DomainEvent],
|
||||
) -> Result<(), DomainError>;
|
||||
/// Tombstones the account in one transaction: stamps `deleted_at`,
|
||||
/// retires the username, drops the user's refresh sessions and records
|
||||
/// `events`. Afterwards every reader below behaves as if the user is gone.
|
||||
async fn mark_deleted(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
events: &[crate::events::DomainEvent],
|
||||
) -> Result<(), DomainError>;
|
||||
/// The tombstoned user, for the purge. Live users are not returned.
|
||||
async fn find_tombstoned_by_id(&self, id: &UserId) -> Result<Option<User>, DomainError>;
|
||||
/// Whether the name belonged to a deleted account (case-insensitive).
|
||||
async fn is_username_retired(&self, username: &Username) -> Result<bool, DomainError>;
|
||||
async fn count_active_admins(&self) -> Result<u64, DomainError>;
|
||||
}
|
||||
|
||||
@@ -51,9 +51,6 @@ pub trait ReviewRepository: Send + Sync {
|
||||
review: &Review,
|
||||
events: &[DomainEvent],
|
||||
) -> Result<(), DomainError>;
|
||||
/// Deletes the review and, in the same transaction, its movie when no
|
||||
/// review of any kind references it any more. `review_events` are always
|
||||
/// recorded; `orphan_events` only when the movie row was deleted.
|
||||
async fn delete_review_and_orphaned_movie(
|
||||
&self,
|
||||
review_id: &ReviewId,
|
||||
|
||||
@@ -10,8 +10,6 @@ pub trait EventPublisher: Send + Sync {
|
||||
async fn publish(&self, event: &DomainEvent) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
/// Delivers one outbox row to the message bus. `Ok` means the bus accepted it
|
||||
/// (for a brokered bus: acknowledged), so the row may be marked done.
|
||||
#[async_trait]
|
||||
pub trait EventRelaySink: Send + Sync {
|
||||
async fn relay(&self, id: Option<&EventId>, event: &DomainEvent) -> Result<(), DomainError>;
|
||||
@@ -23,8 +21,6 @@ pub trait EventConsumer: Send + Sync {
|
||||
|
||||
#[async_trait]
|
||||
pub trait EventHandler: Send + Sync {
|
||||
/// Stable, unique snake_case identity. It keys the processed-event ledger,
|
||||
/// so renaming a handler makes it re-run events still inside the retention window.
|
||||
fn name(&self) -> &'static str;
|
||||
async fn handle(&self, event: &DomainEvent) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ use crate::{
|
||||
|
||||
#[async_trait]
|
||||
pub trait ApDocumentPort: Send + Sync {
|
||||
/// Whether the actor belonged to a deleted account: its documents are
|
||||
/// answered `410 Gone` instead of being served.
|
||||
async fn actor_is_gone(&self, _user_id: Uuid) -> Result<bool, DomainError> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ use crate::{
|
||||
value_objects::{FollowRelation, FollowStatus, SocialActor},
|
||||
};
|
||||
|
||||
/// One row-level change to the local follow tables. A local follow operation
|
||||
/// is a batch of these applied in a single transaction with its events.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum FollowWrite {
|
||||
AddFollow {
|
||||
@@ -42,8 +40,6 @@ pub enum FollowWrite {
|
||||
|
||||
#[async_trait]
|
||||
pub trait FollowCommand: Send + Sync {
|
||||
/// Applies every write and records `events` in one transaction: all rows
|
||||
/// and events land, or none do.
|
||||
async fn apply(
|
||||
&self,
|
||||
writes: &[FollowWrite],
|
||||
|
||||
@@ -11,7 +11,6 @@ pub trait ObjectStorage: Send + Sync {
|
||||
key: &str,
|
||||
) -> Result<futures::stream::BoxStream<'static, Result<bytes::Bytes, DomainError>>, DomainError>;
|
||||
async fn delete(&self, key: &str) -> Result<(), DomainError>;
|
||||
/// Whether exactly `key` exists. Unlike `get`, no extension fallback.
|
||||
async fn exists(&self, key: &str) -> Result<bool, DomainError>;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ impl super::RemoteWatchlistRepository for NoopRemoteWatchlistRepository {
|
||||
|
||||
// ── NoopFederationDeletionNotifier ───────────────────────────────────────────
|
||||
|
||||
/// Federation compiled out: nobody remote to tell.
|
||||
pub struct NoopFederationDeletionNotifier;
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -2,14 +2,8 @@ use async_trait::async_trait;
|
||||
|
||||
use crate::{errors::DomainError, events::EventId};
|
||||
|
||||
/// How long an `in_progress` claim blocks other deliveries of the same
|
||||
/// (event, handler). Past it the claim is presumed dead (crashed worker) and
|
||||
/// the next `begin` takes it over. Must exceed any handler's runtime.
|
||||
pub const HANDLER_LEASE_SECONDS: i64 = 1800;
|
||||
|
||||
/// Proof of ownership of one claim, returned by `begin`. `complete` and
|
||||
/// `release` act only while the claim still carries this token, so an owner
|
||||
/// whose lease lapsed cannot disturb whoever took the claim over.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ClaimToken(String);
|
||||
|
||||
@@ -25,31 +19,15 @@ impl ClaimToken {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Begin {
|
||||
/// The caller now holds the claim and must run the handler, then
|
||||
/// `complete` or `release` with the token.
|
||||
Run(ClaimToken),
|
||||
/// The handler already finished this event; skip it.
|
||||
AlreadyDone,
|
||||
/// Another delivery holds a live claim; do not run. Retry the delivery
|
||||
/// after `retry_after`, when the claim is done or its lease has lapsed.
|
||||
InProgressElsewhere { retry_after: std::time::Duration },
|
||||
}
|
||||
|
||||
/// Per-(event, handler) claim ledger that stops redelivered events from
|
||||
/// re-running handlers.
|
||||
///
|
||||
/// Guarantee: two deliveries never run the same handler for the same event
|
||||
/// concurrently (`begin` is atomic across workers). After a crash mid-handler
|
||||
/// the claim lapses after `HANDLER_LEASE_SECONDS` and the handler runs again,
|
||||
/// so delivery is at-least-once and handlers must be idempotent.
|
||||
#[async_trait]
|
||||
pub trait ProcessedEventLedger: Send + Sync {
|
||||
/// Atomically claims (event, handler): inserts a claim if there is none, or
|
||||
/// takes over an `in_progress` claim older than the lease; otherwise
|
||||
/// reports the existing state.
|
||||
async fn begin(&self, id: &EventId, handler: &str) -> Result<Begin, DomainError>;
|
||||
|
||||
/// Marks the claim done, if `token` still owns it: the handler will not run for this event again.
|
||||
async fn complete(
|
||||
&self,
|
||||
id: &EventId,
|
||||
@@ -57,7 +35,6 @@ pub trait ProcessedEventLedger: Send + Sync {
|
||||
token: &ClaimToken,
|
||||
) -> Result<(), DomainError>;
|
||||
|
||||
/// Drops an `in_progress` claim, if `token` still owns it, so a retry can run immediately.
|
||||
async fn release(
|
||||
&self,
|
||||
id: &EventId,
|
||||
@@ -65,7 +42,5 @@ pub trait ProcessedEventLedger: Send + Sync {
|
||||
token: &ClaimToken,
|
||||
) -> Result<(), DomainError>;
|
||||
|
||||
/// Removes rows (done, or abandoned `in_progress`) first recorded before
|
||||
/// `cutoff`. Returns the number removed.
|
||||
async fn delete_older_than(&self, cutoff: chrono::NaiveDateTime) -> Result<u64, DomainError>;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
use crate::errors::DomainError;
|
||||
use crate::models::DiaryEntry;
|
||||
|
||||
/// Channel-level metadata for a rendered feed. Paths are site-relative
|
||||
/// (e.g. `/users/{id}`); the renderer resolves them against its base URL.
|
||||
pub struct FeedChannel<'a> {
|
||||
pub title: &'a str,
|
||||
/// Human-readable page the channel represents.
|
||||
pub link_path: &'a str,
|
||||
/// Where the feed itself is served, advertised as `atom:link rel="self"`.
|
||||
pub self_path: &'a str,
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ pub trait WatchlistRepository: Send + Sync {
|
||||
events: &[DomainEvent],
|
||||
) -> Result<(), DomainError>;
|
||||
|
||||
/// `events` are recorded only when an entry was actually removed.
|
||||
async fn remove_if_present(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
|
||||
@@ -11,7 +11,6 @@ use crate::value_objects::{
|
||||
|
||||
const DEFAULT_YEAR: u16 = 2000;
|
||||
|
||||
/// Fixed watch timestamp used by review factories (2024-03-15 20:00:00).
|
||||
pub fn test_watched_at() -> NaiveDateTime {
|
||||
NaiveDate::from_ymd_opt(2024, 3, 15)
|
||||
.unwrap()
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
//! Fakes whose every call fails with an infrastructure error, for failure-path tests.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::NaiveDateTime;
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ use crate::models::{
|
||||
};
|
||||
use crate::value_objects::SocialIdentity;
|
||||
|
||||
/// One `processed_events` row.
|
||||
pub(super) struct LedgerRow {
|
||||
pub(super) done: bool,
|
||||
pub(super) started_at: chrono::NaiveDateTime,
|
||||
@@ -22,9 +21,6 @@ pub(super) enum FollowState {
|
||||
Accepted,
|
||||
}
|
||||
|
||||
/// Tables whose rows the schema removes when a user row is deleted
|
||||
/// (`ON DELETE CASCADE` on a column referencing `users(id)`), and which
|
||||
/// `InMemoryStore` models. The sqlite adapter's cascade test asserts the same list.
|
||||
pub const USER_CASCADE_TABLES: &[&str] = &[
|
||||
"watchlist_entries",
|
||||
"goals",
|
||||
@@ -37,11 +33,8 @@ pub const USER_CASCADE_TABLES: &[&str] = &[
|
||||
"blocked_actors",
|
||||
];
|
||||
|
||||
/// Tables that cascade from a user but whose aggregate this store does not hold
|
||||
/// (they belong to the federation crate).
|
||||
pub const USER_CASCADE_TABLES_OUTSIDE_STORE: &[&str] = &["ap_local_actors"];
|
||||
|
||||
/// Tables whose rows the schema removes when a movie row is deleted.
|
||||
pub const MOVIE_CASCADE_TABLES: &[&str] = &[
|
||||
"watchlist_entries",
|
||||
"movie_profiles",
|
||||
@@ -51,17 +44,12 @@ pub const MOVIE_CASCADE_TABLES: &[&str] = &[
|
||||
"movie_crew",
|
||||
];
|
||||
|
||||
/// One in-memory implementation of every persistence port, mirroring the
|
||||
/// relational constraints of the sqlite schema (cascades, uniqueness, FK
|
||||
/// restrictions). Share one `Arc<InMemoryStore>` across all port slots.
|
||||
pub struct InMemoryStore {
|
||||
pub(super) movies: RwLock<HashMap<Uuid, Movie>>,
|
||||
pub(super) movie_profiles: RwLock<HashMap<Uuid, MovieProfile>>,
|
||||
pub(super) reviews: RwLock<HashMap<Uuid, Review>>,
|
||||
pub(super) users: RwLock<HashMap<Uuid, User>>,
|
||||
/// Ids of `users` rows carrying a `deleted_at` tombstone.
|
||||
pub(super) tombstoned: RwLock<std::collections::HashSet<Uuid>>,
|
||||
/// Lower-cased `retired_usernames` (the column is NOCASE).
|
||||
pub(super) retired_usernames: RwLock<std::collections::HashSet<String>>,
|
||||
pub(super) profile_fields: RwLock<HashMap<Uuid, Vec<ProfileField>>>,
|
||||
pub(super) user_settings: RwLock<HashMap<Uuid, UserSettings>>,
|
||||
@@ -111,8 +99,6 @@ impl InMemoryStore {
|
||||
store
|
||||
}
|
||||
|
||||
/// Events recorded atomically with a change (the outbox rows the sqlite
|
||||
/// adapter would have written in the same transaction), in write order.
|
||||
pub fn recorded_events(&self) -> Vec<crate::events::DomainEvent> {
|
||||
self.recorded_events.read().unwrap().clone()
|
||||
}
|
||||
@@ -156,7 +142,6 @@ impl InMemoryStore {
|
||||
self.processed_events.read().unwrap().len()
|
||||
}
|
||||
|
||||
/// Inserts a ledger row with explicit timestamps (retention and lease tests).
|
||||
pub fn seed_processed_event(
|
||||
&self,
|
||||
id: &crate::events::EventId,
|
||||
@@ -178,13 +163,10 @@ impl InMemoryStore {
|
||||
self.refresh_sessions.read().unwrap().len()
|
||||
}
|
||||
|
||||
/// Inserts a user bypassing the uniqueness checks of `UserRepository::save`.
|
||||
pub fn seed_user(&self, user: User) {
|
||||
self.users.write().unwrap().insert(user.id().value(), user);
|
||||
}
|
||||
|
||||
/// Row count of the sqlite table of the same name, for the tables this
|
||||
/// store models. Panics on a table the store does not hold.
|
||||
pub fn rows_in(&self, table: &str) -> usize {
|
||||
match table {
|
||||
"watchlist_entries" => self.watchlist_count(),
|
||||
|
||||
@@ -12,7 +12,6 @@ use super::InMemoryStore;
|
||||
|
||||
#[async_trait]
|
||||
impl GoalCommand for InMemoryStore {
|
||||
/// `goals` is UNIQUE(user_id, year).
|
||||
async fn save(&self, goal: &Goal, events: &[DomainEvent]) -> Result<(), DomainError> {
|
||||
let mut goals = self.goals.write().unwrap();
|
||||
let duplicate = goals.values().any(|g| {
|
||||
|
||||
@@ -15,9 +15,6 @@ use super::InMemoryStore;
|
||||
|
||||
#[async_trait]
|
||||
impl MovieCommand for InMemoryStore {
|
||||
/// `movies.external_metadata_id` is UNIQUE: another movie row holding the
|
||||
/// same external id is a constraint violation, which the sqlite adapter
|
||||
/// surfaces as an infrastructure error.
|
||||
async fn upsert_movie(&self, movie: &Movie, events: &[DomainEvent]) -> Result<(), DomainError> {
|
||||
let mut movies = self.movies.write().unwrap();
|
||||
if let Some(external) = movie.external_metadata_id() {
|
||||
@@ -37,9 +34,6 @@ impl MovieCommand for InMemoryStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Schema: `reviews.movie_id REFERENCES movies(id)` has no cascade, so a
|
||||
/// movie with reviews cannot be deleted. `watchlist_entries` and the
|
||||
/// `movie_*` profile tables cascade; `watch_events.movie_id` is set to NULL.
|
||||
async fn delete_movie(
|
||||
&self,
|
||||
movie_id: &MovieId,
|
||||
@@ -67,7 +61,6 @@ impl MovieCommand for InMemoryStore {
|
||||
}
|
||||
|
||||
impl InMemoryStore {
|
||||
/// Removes the movie row and applies the schema's cascades.
|
||||
pub(super) fn remove_movie_with_cascades(&self, id: uuid::Uuid) {
|
||||
self.movies.write().unwrap().remove(&id);
|
||||
self.watchlist
|
||||
@@ -155,7 +148,6 @@ impl MovieQuery for InMemoryStore {
|
||||
page: &PageParams,
|
||||
filter: &MovieFilter,
|
||||
) -> Result<Paginated<MovieSummary>, DomainError> {
|
||||
// Mirrors the sqlite adapter: filter, ORDER BY title ASC, then page.
|
||||
let movies = self.movies.read().unwrap();
|
||||
let profiles = self.movie_profiles.read().unwrap();
|
||||
let search = filter.search.as_deref().map(str::to_lowercase);
|
||||
|
||||
@@ -10,7 +10,6 @@ use crate::{
|
||||
use super::InMemoryStore;
|
||||
use super::store::LedgerRow;
|
||||
|
||||
/// Claim stamps are millisecond-precision UTC, as in the sqlite adapter.
|
||||
const STAMP: &str = "%Y-%m-%dT%H:%M:%S%.3fZ";
|
||||
|
||||
fn now_millis() -> NaiveDateTime {
|
||||
@@ -23,9 +22,6 @@ fn token(at: NaiveDateTime) -> ClaimToken {
|
||||
ClaimToken::new(at.format(STAMP).to_string())
|
||||
}
|
||||
|
||||
/// Mirrors the sqlite adapter: `begin` inserts, or takes over only a stale
|
||||
/// `in_progress` claim; `(event_id, handler)` is the key; `complete` and
|
||||
/// `release` act only for the token that owns the claim.
|
||||
#[async_trait]
|
||||
impl ProcessedEventLedger for InMemoryStore {
|
||||
async fn begin(&self, id: &EventId, handler: &str) -> Result<Begin, DomainError> {
|
||||
|
||||
@@ -10,7 +10,6 @@ use super::InMemoryStore;
|
||||
|
||||
#[async_trait]
|
||||
impl RefreshSessionRepository for InMemoryStore {
|
||||
/// `refresh_sessions.token` is UNIQUE.
|
||||
async fn create(&self, session: &RefreshSession) -> Result<(), DomainError> {
|
||||
let mut sessions = self.refresh_sessions.write().unwrap();
|
||||
if sessions.iter().any(|s| s.token == session.token) {
|
||||
|
||||
@@ -34,8 +34,6 @@ impl UserRepository for InMemoryStore {
|
||||
.cloned())
|
||||
}
|
||||
|
||||
/// `users.email` and `users.username` are UNIQUE; the sqlite adapter checks
|
||||
/// both up front and reports a validation error.
|
||||
async fn save(&self, user: &User) -> Result<(), DomainError> {
|
||||
let mut users = self.users.write().unwrap();
|
||||
if users
|
||||
@@ -129,8 +127,6 @@ impl UserRepository for InMemoryStore {
|
||||
.count() as u64)
|
||||
}
|
||||
|
||||
/// Ordered by email ascending like the real query. Diary stats are stubbed
|
||||
/// (review count 0, no last activity); only the identity fields are real.
|
||||
async fn list_with_stats(&self) -> Result<Vec<UserSummary>, DomainError> {
|
||||
let store = self.users.read().unwrap();
|
||||
let gone = self.tombstoned.read().unwrap();
|
||||
@@ -229,10 +225,6 @@ impl UserFederationSettingsQuery for InMemoryStore {
|
||||
}
|
||||
|
||||
impl InMemoryStore {
|
||||
/// Deletes a user the way `DELETE FROM users` does under the production
|
||||
/// schema: rows in `USER_CASCADE_TABLES` go with it. Reviews, refresh
|
||||
/// sessions, import sessions and import profiles carry a plain `user_id`
|
||||
/// column with no foreign key, so they stay.
|
||||
pub fn delete_user(&self, user_id: &UserId) {
|
||||
let id = user_id.value();
|
||||
self.users.write().unwrap().remove(&id);
|
||||
|
||||
@@ -16,8 +16,6 @@ use super::InMemoryStore;
|
||||
|
||||
#[async_trait]
|
||||
impl WatchlistRepository for InMemoryStore {
|
||||
/// `INSERT OR IGNORE` on UNIQUE(user_id, movie_id): adding twice is a no-op
|
||||
/// and, like the adapter, records events only for the row it inserted.
|
||||
async fn add(&self, entry: &WatchlistEntry, events: &[DomainEvent]) -> Result<(), DomainError> {
|
||||
let key = (entry.user_id.value(), entry.movie_id.value());
|
||||
let inserted = {
|
||||
|
||||
@@ -81,8 +81,6 @@ impl InMemoryWrapUpRepository {
|
||||
})
|
||||
}
|
||||
|
||||
/// Events recorded atomically with a change (the outbox rows the sqlite
|
||||
/// adapter writes in the same transaction).
|
||||
pub fn recorded_events(&self) -> Vec<DomainEvent> {
|
||||
self.recorded.lock().unwrap().clone()
|
||||
}
|
||||
@@ -268,9 +266,6 @@ impl WrapUpRepository for PanicWrapUpRepository {
|
||||
|
||||
// ── RefusingWrapUpWrites ───────────────────────────────────────────────────
|
||||
|
||||
/// Delegates to an in-memory repository but refuses the two writes that carry
|
||||
/// outbox events (`create`, `set_complete`), leaving `update_status` and reads
|
||||
/// working so a refusal can be observed mid-flow.
|
||||
pub struct RefusingWrapUpWrites {
|
||||
pub inner: Arc<InMemoryWrapUpRepository>,
|
||||
}
|
||||
|
||||
@@ -33,8 +33,6 @@ impl MovieId {
|
||||
uuid_id!(ReviewId);
|
||||
uuid_id!(UserId);
|
||||
|
||||
/// Id of the instance's service actor (seeded by migration 0022). It signs
|
||||
/// fetches for the whole instance and is never a deletable account.
|
||||
pub const SERVICE_ACTOR_UUID: Uuid =
|
||||
Uuid::from_bytes([0, 0, 0, 0, 0, 0, 0x40, 0, 0x80, 0, 0, 0, 0, 0, 0, 0]);
|
||||
|
||||
|
||||
@@ -61,8 +61,6 @@ fn movie_profile(movie_id: &MovieId) -> MovieProfile {
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves a user and one row in every table the schema cascades from a user,
|
||||
/// plus rows in the tables that hang off a user without a foreign key.
|
||||
async fn seed_user_with_dependents(store: &Arc<InMemoryStore>) -> (UserId, Movie) {
|
||||
let user = test_user("alice");
|
||||
let user_id = user.id().clone();
|
||||
|
||||
Reference in New Issue
Block a user