# Lifecycle

Install, uninstall, TTL, delete cascades — how state gets created, maintained, and torn down.

## Owner projection

Owner modules (`wow_character`, `wow_guild`, `wow_user`) project reference fields onto entities owned by OTHER modules.

The flow has two paths that produce the same end state:

### Path A — owner installed FIRST

```
wow_character install
  ↓
wow_character_install() runs — iterates _wow_character_owned_entity_types()
  ↓
for each entity type that exists: installFieldStorageDefinition('character', ...)
  ↓
(later) wow_achievement install
  ↓
wow_achievement_progress schema is built
  ↓
hook_entity_base_field_info fires → wow_character contributes the 'character' base field
  ↓
column added automatically during schema creation
```

### Path B — owner installed LATER

```
wow_achievement install
  ↓
wow_achievement_progress schema built, no 'character' column
  ↓
(later) wow_character install
  ↓
wow_character_install() iterates owned types, finds wow_achievement_progress
  ↓
installFieldStorageDefinition('character', 'wow_achievement_progress', ...)
  ↓
column added to the existing schema
```

Both paths are tested in `CharacterFieldInjectionOrderingTest`, `UserFieldInjectionOrderingTest`, and `GuildFieldInjectionOrderingTest`.

## Uninstall

Uninstalling an owner module drops its projection cleanly:

- Schema columns are removed from every target entity type.
- Rows with the projected owner populated are purged first (so the projection column has nothing to migrate through Drupal's field-storage delete hooks).
- Orphan rows (projection column NULL) survive.
- Catalog entities owned by other modules are untouched.

Uninstalling a catalog module goes further: its own base tables, state keys, and `user.data` namespace (where applicable) are also removed.

## The 30-day TTL

Blizzard's Terms of Use require API-derived data to be refreshed or deleted within 30 days. The `wow.ttl_sweeper` service enforces this:

- Runs daily under `hook_cron`, throttled by the `wow.ttl_sweep_last` state key.
- On demand: `drush wow:sweep-ttl`.
- Sweeps every content entity type that declares a `last_fetched` base field — catalogs AND junction entities.
- Taxonomy reference data (which uses a `wow_last_fetched` *configurable* field because `taxonomy_term` can't host arbitrary base fields) is NOT swept; operators keep it fresh via the scheduled sync commands.

Junction entities have their own `last_fetched` independent of their owning character/guild. Two scenarios make this matter:

1. Blizzard's character profile API can return a partial collection on big accounts — untouched rows go stale while the character itself is still fresh.
2. When a player un-collects a mount/pet/title, Blizzard stops returning that row; the local junction row sits frozen until swept.

A regression test (`TtlSweepCoverageTest`) scans every entity class for `last_fetched` and asserts each is registered in `TtlSweeper::SWEEP_ENTITY_TYPES`. Adding a new catalog entity without wiring the sweep fails CI.

## Delete cascades

Three cascades, all using `hook_entity_predelete`:

### Character delete → CharacterDataProvider purges

```
Character::delete()
  ↓
wow_character_entity_predelete()
  ↓
for each CharacterDataProvider:
  try { deleteCharacterData($character); } catch log & continue
```

Every registered provider purges its per-character rows. One provider throwing doesn't block the rest; failures are logged with `@id`, `@cid`, `@name`, `@msg` context.

### Guild delete → GuildDataProvider purges

Same shape, via `wow_guild_entity_predelete()` and `GuildDataProvider` plugins.

### User delete / de-link → character purge → downstream cascade

Two entry points that converge on `wow_user_purge_account_data($uid)`:

1. **Account cancellation** — `wow_user_entity_predelete()` on the user entity.
2. **Battle.net de-link** — form_alter + submit handler on `openid_connect_accounts_form`, scoped to the `battlenet` client only.

Both call the same purge function which:
- deletes every `wow_character` row owned by the user (which in turn cascades to all CharacterDataProvider plugins),
- clears `user.data` under `wow_user` and `wow_battlenet_login`.

## Rate limiting

Two windows, both in Drupal State so all PHP processes share one counter:

- `wow.api_rate_limit` — hourly (36,000 requests)
- `wow.api_rate_limit_second` — per-second (100 requests)

Check + increment is serialized under the Drupal lock service (`wow:api_rate_limit`). If the lock can't be acquired after a few retries, the request is throttled — the client refuses to mutate counters with no guard held.

This closes the last concurrent-process race. See `BattleNetClient::reserveRateSlot()` and `RateLimitIntegrationTest` / `BattleNetClientRateLimitTest`.
