Skip to content

Commit 16eadf1

Browse files
committed
feat(api): surface feed last-updated as update_time on catalogue resources
The upstream feeds carry a last-updated timestamp the contract was discarding: beverage files have a per-category top-level `timestamp`, and the registry has a top-level `last_updated`. Expose them as OUTPUT_ONLY update_time: - Drink.update_time / Producer.update_time <- beverage file timestamp (per festival category; the file is the unit of change, so items in a category share the value) - Festival.update_time <- registry last_updated (registry-wide) Document the freshness/polling model in the proto README: conditional re-fetch (If-None-Match/304) keyed off update_time, not item-level delta (the feeds are whole-file snapshots, so per-drink change attribution isn't available). buf lint, AIP api-linter, OpenAPI generation, and buf breaking all pass.
1 parent 6869b93 commit 16eadf1

4 files changed

Lines changed: 53 additions & 0 deletions

File tree

proto/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,36 @@ service, graduate at different times while staying under
5151

5252
See `buf.yaml` for the step-by-step promotion path.
5353

54+
### Freshness & polling
55+
56+
Catalogue data changes during a live festival (drink availability especially)
57+
and between festivals (new festivals). Clients poll the List endpoints to stay
58+
current; the contract carries the freshness signal the upstream feeds already
59+
provide:
60+
61+
- Each beverage feed file (`{festival}/{category}.json`) has a top-level
62+
`timestamp` (last-updated). It surfaces as **`Drink.update_time`** and
63+
**`Producer.update_time`** — granularity is per festival category (the file is
64+
the unit of change), so all items from one category share the value. The feed
65+
does **not** expose per-drink change times.
66+
- The registry feed has a top-level `last_updated`, surfaced as
67+
**`Festival.update_time`** (registry-wide).
68+
69+
The efficient polling model is therefore **conditional re-fetch**, not item-level
70+
delta sync:
71+
72+
1. Poll with HTTP `If-None-Match`/`If-Modified-Since`; an unchanged collection
73+
returns `304 Not Modified` and transfers nothing.
74+
2. When it has changed, re-pull the (small, bounded) list and use `update_time`
75+
to confirm/raise the client's "as of" marker and drive "updated N ago" UI.
76+
77+
Item-level delta (`update_time > T` filtering) is intentionally **not** offered:
78+
the feeds are whole-file snapshots, so the server cannot attribute a change to a
79+
single drink. Re-fetch keyed off the per-category `update_time` matches the data's
80+
real change granularity. (Realising the `304` path needs the worker to honour
81+
conditional requests and shorten the drinks `Cache-Control` TTL — tracked
82+
separately from this contract.)
83+
5484
## Layout
5585

5686
```

proto/cambeerfestival/festival/v1alpha/drink.proto

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package cambeerfestival.festival.v1alpha;
55

66
import "google/api/field_behavior.proto";
77
import "google/api/resource.proto";
8+
import "google/protobuf/timestamp.proto";
89

910
// A single beverage available at a festival, flattened from the underlying
1011
// data feed's producer/product structure. Product details live here; the
@@ -72,6 +73,14 @@ message Drink {
7273
// The brewery/cidery/meadery that makes this drink, as a reference to the
7374
// Producer resource plus its display name for cheap list rendering.
7475
ProducerReference producer = 13 [(google.api.field_behavior) = OUTPUT_ONLY];
76+
77+
// When this drink's source dataset was last updated, taken from the feed
78+
// file's top-level `timestamp`. Granularity is per festival category (the
79+
// feed file is the unit of change), so all drinks from the same category
80+
// share this value — the feed does not expose per-drink change times. Use it,
81+
// alongside HTTP Last-Modified/ETag, to detect whether a category's data
82+
// changed and decide whether to re-poll.
83+
google.protobuf.Timestamp update_time = 14 [(google.api.field_behavior) = OUTPUT_ONLY];
7584
}
7685

7786
// A lightweight pointer from a Drink to its Producer: the producer's resource

proto/cambeerfestival/festival/v1alpha/festival.proto

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package cambeerfestival.festival.v1alpha;
55

66
import "google/api/field_behavior.proto";
77
import "google/api/resource.proto";
8+
import "google/protobuf/timestamp.proto";
89
import "google/type/date.proto";
910
import "google/type/latlng.proto";
1011

@@ -80,4 +81,11 @@ message Festival {
8081

8182
// Donation URL for the charity partner. Empty when none.
8283
string charity_donation_uri = 16 [(google.api.field_behavior) = OUTPUT_ONLY];
84+
85+
// When the festival registry/config was last updated, taken from the registry
86+
// feed's top-level `last_updated`. This reflects festival *metadata*
87+
// freshness and is registry-wide (the same across festivals in one response);
88+
// per-drink and per-producer data freshness is on those resources'
89+
// update_time.
90+
google.protobuf.Timestamp update_time = 17 [(google.api.field_behavior) = OUTPUT_ONLY];
8391
}

proto/cambeerfestival/festival/v1alpha/producer.proto

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package cambeerfestival.festival.v1alpha;
55

66
import "google/api/field_behavior.proto";
77
import "google/api/resource.proto";
8+
import "google/protobuf/timestamp.proto";
89

910
// A producer (brewery, cidery, meadery, etc.) at a festival.
1011
//
@@ -39,4 +40,9 @@ message Producer {
3940

4041
// Free-text notes about the producer. Empty when unset.
4142
string notes = 5 [(google.api.field_behavior) = OUTPUT_ONLY];
43+
44+
// When this producer's source dataset was last updated, taken from the feed
45+
// file's top-level `timestamp` (producers come from the same category feed
46+
// files as drinks). Granularity is per festival category, not per producer.
47+
google.protobuf.Timestamp update_time = 6 [(google.api.field_behavior) = OUTPUT_ONLY];
4248
}

0 commit comments

Comments
 (0)