@gamesheet-inc/cache-invalidation-contract
v0.1.0
Published
The stats widget's cache-invalidation contract: cache tags and URL rules, generation keys, and the cache-invalidate message v1.
Readme
Cache invalidation contract
@gamesheet-inc/cache-invalidation-contract is this contract's only copy. The edge-router and the
widget app import it through the app-stats-widget-next workspace; cache-invalidation-service and
project-forge pin a published version. The data-write-gateway's Go struct
(src/Shared/Invalidation/message.go) mirrors §3 by hand, so a message change lists it among the
consumers to update. Design: docs/superpowers/specs/2026-09-28-cache-invalidation-contract-package-design.md
in app-stats-widget-next, which builds on the cache-invalidation design spec §7.
1. Cache tags (edge-router ↔ service)
Every response the edge Worker caches carries cf.cacheTags, derived from the public URL only
(the Worker's own 60 s build-id lookup is the single untagged entry; see the edge-router
constraints doc):
<env>-season-<id><env>-league-<id><env>-game-<id><env>-team-<id><env>-player-<id><env>-widget-config-<id>- constant
<env>-stats-widgeton every cached response
<env> is one of dev | qa | prod — the Worker's TAG_ENV var and the service's TAG_ENV
var, which must match per env (the apex and next. prod Workers both use prod). Ids are decimal
integers exactly as they appear in URLs. Tags are emitted lower-case; Cloudflare matches them
case-insensitively. A Worker with no valid TAG_ENV emits no tags at all — never an unprefixed one.
Why the prefix: Cloudflare cache tags are zone-wide, dev/qa/prod share gamesheetstats.com, and the
three databases have overlapping numeric id spaces. Without it a dev purge of season-19483 would
evict prod's season 19483.
<env>-stats-widget is a break-glass lever (purge one env's whole widget edge cache) with its own
runbook in the service repo. It is never exposed in a UI.
2. Generation keys (widget ↔ service; the widget's Upstash instance, per env)
gen:season:<id>gen:league:<id>gen:game:<id>gen:player:<id>gen:widget-config:<id>
Integer counters. A missing key reads as 0. The service is the only writer (INCR + EXPIRE
30 days on every bump); the widget only reads. There is no gen:team:* — every team-scoped widget
key also carries its season, so the season counter covers it.
3. cache-invalidate topic payload v1 (publishers ↔ service)
{
"v": 1,
"source": "data-write-gateway.ScheduleGame.update",
"reason": "optional free text",
"seasons": [19483],
"games": [123],
"teams": [456, 789],
"leagues": [],
"players": [],
"widgetConfigs": []
}Message attributes: schema: cache-invalidate.v1, source. All arrays optional. Publishers send
integer ids; the schema also accepts numeric strings of up to 19 digits and parses them. Every
id must parse to a positive integer of at most 9007199254740991 (Number.MAX_SAFE_INTEGER);
anything else is refused.
Publishers never send tags or keys.
Exports
- Tags:
TAG_ENVS,TagEnv,parseTagEnv,SCOPE_TAG_KINDS,ScopeTagKind,STATS_WIDGET_TAG,tagFor,breakGlassTag. - URL rules:
ScopeRef,resolveScopes,resolveCacheTags. - Generation keys:
GEN_KINDS,GenKind,GEN_KEY_PREFIX,genKeyFor,GEN_TTL_SECONDS. - Message v1:
CACHE_INVALIDATE_TOPIC,CACHE_INVALIDATE_SCHEMA,MESSAGE_FIELDS,MessageField,ScopeIdSchema,CacheInvalidateV1Schema,CacheInvalidateV1. zod is a required peer.
Changelog
A change to any exported value or rule is a new version. Each entry names the consumers that must move: the edge-router and the widget app (same PR, through the workspace), cache-invalidation-service and project-forge (a pin bump), and the data-write-gateway's Go struct (by hand, for a message change).
- 0.1.0 — First release. Moves the edge-router's
cache-tags.ts, the widget's generation-key names and the service'scontract.ts,idSchemaandcacheInvalidateV1Schemahere, with no change to any tag, key or accepted message.
