feat: bounded SSE feeds for offer & auction state machines
New text/event-stream endpoints — /o/{offer_id}/events (buyer/seller only)
and /a/{auction_id}/events (public). Each polls the row ~every 1.5s, emits
a `data: {json}` frame on connect and whenever the state-machine state
changes, sends a heartbeat comment, then closes after ~25s so the browser
EventSource reconnects — "bounded" because uWSGI is sync (~16 worker
threads) and a long-lived SSE would starve the pool. Shared helper
lib/sse.py (sse_response / event_stream); it uses its own short-lived DB
session per poll (request.dbsession is already closed by pyramid_tm by the
time the streaming generator runs). Timings come from settings
(app.sse.hold_seconds / app.sse.poll_interval_seconds; test.ini sets them
tiny so the streaming tests finish in ~0.06s).
Client: auction.js opens the EventSource and feeds each frame into its
existing applyState(); it falls back to polling /a/{id}.json every 5s
where EventSource is unavailable. offer.js opens the EventSource on the
offer page and reload()s on a state change (the whole layout depends on
state / can_act). offer.j2 carries data-offer-state. Caddy auto-detects
text/event-stream and stops buffering — no Salt change.
Tests: 4 new functional tests (both endpoints stream the right
content-type + a state frame; 404 for outsiders / unknown ids). 994 passed.
This commit is contained in:
parent
93c0facbbc
commit
157e6f769a
12 changed files with 344 additions and 10 deletions
|
|
@ -78,7 +78,8 @@ window means 1-min granularity is fine.
|
|||
|
||||
```
|
||||
GET /a/{auction_id} live page (auction.j2 + auction.js)
|
||||
GET /a/{auction_id}.json JSON state for poll
|
||||
GET /a/{auction_id}.json JSON state (one-shot; fallback poll)
|
||||
GET /a/{auction_id}/events bounded SSE feed of auction state (public)
|
||||
POST /a/{auction_id}/bid place bid (login required, no self-bid)
|
||||
POST /a/{auction_id}/buy-now end auction at buy_now (mode 2)
|
||||
POST /a/{auction_id}/watch toggle watcher
|
||||
|
|
@ -96,7 +97,14 @@ gift-card-purchases. After standard cart payment success,
|
|||
## Live UI (`static/js/auction.js`)
|
||||
|
||||
- Countdown clock ticks every 1s (data-end-timestamp attribute)
|
||||
- Polls `/a/{id}.json` every 5s for state changes
|
||||
- Live state via a **bounded SSE feed** `/a/{id}/events` (`auction_events`
|
||||
view → `lib/sse.py`): polls the row ~every 1.5s, emits a `data:` frame
|
||||
on connect and on bid/soft-close/state change, heartbeats, then closes
|
||||
after ~25s so `EventSource` reconnects (uWSGI sync workers can't hold
|
||||
long-lived connections). `auction.js` calls `applyState()` per frame.
|
||||
Where `EventSource` is unavailable it falls back to polling
|
||||
`/a/{id}.json` every 5s. The countdown is NOT part of the SSE change
|
||||
signal — the client derives it from `end_timestamp`.
|
||||
- AJAX bid submit; success/error flash without page reload
|
||||
|
||||
The page works fully without JS (capability-driven presentation): `bid`,
|
||||
|
|
|
|||
|
|
@ -112,8 +112,26 @@ POST /o/{offer_id}/decline decline current amount (terminal)
|
|||
POST /o/{offer_id}/withdraw buyer-only terminal pull
|
||||
POST /o/{offer_id}/checkout buyer pays accepted offer
|
||||
GET /s/{shop_id}/offers operator inbox of all offers for the shop
|
||||
GET /o/{offer_id}/events bounded SSE feed of the offer's state (buyer/seller only)
|
||||
```
|
||||
|
||||
### Live updates (bounded SSE)
|
||||
|
||||
`/o/{offer_id}/events` is a `text/event-stream` that polls the offer row
|
||||
every ~1.5s, emits a `data: {json}` frame on connect and whenever the
|
||||
state-machine state changes, sends a heartbeat comment, then closes after
|
||||
~25s — the browser `EventSource` reconnects. This caps the worker-thread
|
||||
hold per client (uWSGI is sync, ~16 threads; a truly long-lived SSE would
|
||||
starve the pool). Shared helper: `lib/sse.py` (`sse_response` /
|
||||
`event_stream`). Timings are settings (`app.sse.hold_seconds`,
|
||||
`app.sse.poll_interval_seconds`; `test.ini` sets them tiny). The
|
||||
generator uses its **own** short-lived DB session per poll (not
|
||||
`request.dbsession`, which pyramid_tm has already closed by then).
|
||||
`offer.js` opens the `EventSource` on the offer page and `reload()`s on a
|
||||
state change (the whole page layout depends on state/`can_act`). Auctions
|
||||
have the same: `/a/{auction_id}/events` (public) + `auction.js` calls
|
||||
`applyState()` on each frame.
|
||||
|
||||
`offer_open` is registered before the `product_slug` catch-all so
|
||||
`/p/{id}/offer` is not shadowed.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue