---
title: Consolidate the plays-view cache lifecycle into one executor
type: task
status: planned
description: >-
  Give the plays view a single executor owning cache read, execute and write, so the export path
  stops reading a cache it never writes. Also removes the redundant /data-views/search redis read.
base_uri: user:task/league/data-views/consolidate-plays-view-cache-lifecycle.md
created_at: '2026-09-22T04:28:58.543Z'
entity_id: f5503449-01c7-483c-8262-4ae58e55c025
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
tags:
  - user:tag/league-xo-football.md
  - user:tag/software-task.md
updated_at: '2026-09-22T05:43:04.970Z'
visibility_analyzed_at: '2026-11-05T23:48:46.226Z'
---

# Consolidate the Plays-View Cache Lifecycle

## Why this is separate

Split out of [[user:task/league/data-views/shorten-view-cache-ttl-inside-live-game-windows.md]] during review, because bundling it doubled the blast radius of a freshness change.

**Ordering is load-bearing: this must land AFTER that task, not before.** It gives the plays-view export a cache write it does not have today. Until the live-game-window TTL rules exist, adding that write would make live exports STALER rather than fresher — an export that currently re-queries every time would start serving a cached body under the long per-source TTL.

That predecessor has now shipped, so this is unblocked.

## Scope

- **One plays-view executor owning read, execute and write.** The lifecycle is currently spread across three call sites with no shared owner: `api/routes/plays.mjs` (`POST /views/search`), `api/sockets/plays_view.mjs`, and the export route. The data view already has this shape in `libs-server/data-views/execute-data-view-request.mjs` — including admission, telemetry and a `cache_set` seam — and is the model to follow.
- **The export's missing cache write at `api/routes/plays.mjs:765`.** It READS the cache and never writes it, so every export is a full re-execution.
- **The redundant `/data-views/search` redis read at `api/routes/data-views.mjs:1323`.** The executor already reads the cache, so the route's own read is a second lookup on every request.

## What the predecessor left here deliberately

The live-game-window TTL floor is applied at BOTH plays-view write sites today (`api/routes/plays.mjs` and `api/sockets/plays_view.mjs`), each with its own `Date.now()` pair to measure execution. That duplication is the thing a single executor collapses. Keep the behavior identical when consolidating:

- `plays_view_metadata.live_covering` is published by the fold in `libs-server/plays-view/get-plays-view-results.mjs`; the executor applies `apply_live_game_window_ttl_floor_seconds` at the write.
- **This subsystem's `cache_ttl` is SECONDS.** `data_view_metadata.cache_ttl` is MILLISECONDS. Do not unify the two by copying the data view's conversion — verify each boundary, since a units bug in exactly this metadata has shipped before.

## Verify

A control and a perturbed case must produce different readings on every step. Specifically: assert the export path WRITES the cache after consolidation by asserting a second identical export is a cache hit, and assert it takes the live-window floor inside a game window and the ordinary TTL outside one — the two TTLs must differ. A body-equality check is not evidence here, because a correct implementation can refresh and return an identical body.

## Plan

- [ ] **Build the plays-view executor**
  - File: `libs-server/plays-view/execute-plays-view-request.mjs` (new)
  - Changes: one entry point owning cache read, execute and write, modelled on `libs-server/data-views/execute-data-view-request.mjs`. Measure execution once inside it and apply `apply_live_game_window_ttl_floor_seconds` at the write. Expose a `cache_set` seam so a spec can assert the TTL without redis.
  - Verify: spec asserting a live-covering metadata takes the floor and a non-live-covering one takes the untouched TTL, with the two readings differing.

- [ ] **Route the search and socket paths through it**
  - Files: `api/routes/plays.mjs`, `api/sockets/plays_view.mjs`
  - Changes: replace both hand-rolled read/execute/write blocks, including their duplicated `Date.now()` timing pairs, with one executor call.
  - Verify: existing plays-view specs stay green, and confirm the TTL written is unchanged for a non-live-covering view — a refactor that silently moves the TTL is the failure to catch.

- [ ] **Give the export a cache write**
  - File: `api/routes/plays.mjs` around `:765`
  - Changes: the export reads the cache and never writes it, so route it through the executor.
  - Verify: a second identical export is a cache HIT where it is currently a full re-execution, and inside a game window its key's TTL is the live floor rather than hours. Assert on the TTL and on hit-versus-miss, never on body equality — a correct refresh can return an identical body.

- [ ] **Remove the redundant `/data-views/search` redis read**
  - File: `api/routes/data-views.mjs` around `:1323`
  - Changes: the executor already reads the cache, so drop the route's own lookup.
  - Verify: cache-hit behavior is unchanged for a repeated search, and the redis read count per request drops by one.

- [ ] **Gates**
  - Changes: `yarn test:isolated` scoped to the touched specs, `yarn lint`, `yarn check:types`, `npx prettier --check` on the touched files.
  - Verify: assert on the presence of the `N passing` summary line and the logged exit code, not on the absence of failures.
