Working together
Several people can have Bottle open at once. People are never locked out of anything: they see each other instead. Presence says who is on which page, pointers show where someone is pointing on the rounds map, and planning activity says who is working on which day. Two things keep changes safe: a version check on whole-round changes, and assistants giving way to people.
All three live in the API’s memory, not the database. They are only ever true for the next minute or two, and a restart forgetting them costs nothing. The API runs as one process; a second replica would move them to the database’s NOTIFY, with the same shapes.
The live stream
Section titled “The live stream”GET /events is a server-sent event stream for a business. Besides change
events (something under a path changed, so re-read it), it carries:
| Event | Data |
|---|---|
presence |
{ items: Presence[] }: every open tab in the business. |
cursor |
{ tabId, page, latitude, longitude, userId, name, at }: one pointer move. |
planning |
{ date, planners: PlanningPerson[] }: who is planning that day now. |
The stream is for sessions only. Keys and connections poll.
Presence
Section titled “Presence”A tab makes up its own tabId and checks in with POST /presence
({ tabId, page, focus, state }) on arriving, on changing page or what it has open,
and every twenty seconds. focus is what it has open: on Rounds, the day and round as date/roundId,
which is what lets somebody follow along. state is active (in front and
used in the last two minutes), idle (in front, untouched) or away (open in
a tab that is not in front); a tab checks in as soon as it changes. The answer, and every presence event, is the whole list. A tab that
stops checking in drops off after fifty seconds; DELETE /presence/{tabId}
takes it off at once. Sessions only, under tenant:read.
Pointers
Section titled “Pointers”POST /presence/cursor with { tabId, page, latitude, longitude, anchors, holding } passes a pointer on as a cursor event. Over the map it carries the
point; over anything else it carries anchors: the named parts of the page
under the pointer (marked data-follow in the web app, such as a round’s
stop or the waiting list), innermost first, each with how far across and down
it the pointer is. A screen places the pointer on the first of those parts it
has, so it lands on the same row whatever the screen’s size or scroll. holding is the order being dragged,
if one is, so the others see what is moving. It is not kept. Pointers are points on the
map, not pixels, so everyone sees them in the same place whatever their
screen. The web app sends at most about eight a second, and drops a pointer
that has not moved for six seconds.
Who is planning, and assistants giving way
Section titled “Who is planning, and assistants giving way”Every planning change a person makes to a day marks them as planning it for
the next two minutes. GET /planning-activity?date= lists them, and changes
arrive on the stream as planning events.
People are never stopped by each other. An API key or connected assistant’s
planning change to a day is refused while any person is planning it, with
409 and error: "beingPlanned", and a message naming who. A system must not
change what somebody is in the middle of making. It can try again once the day
has been quiet for two minutes. Drivers are never counted or stopped, and
taking an order is not planning.
Planning changes are: creating and deleting rounds, planning a whole day, changing a round, working out its order again, moving an order on or off a round, sending a round to its driver or taking it back, and marking a driver off for a day.
The version check
Section titled “The version check”Every round has a version, which changes whenever its driver, van, start or
end, status, departure time, or stops and their order change. Send it back as
If-Match on the calls that change a whole round:
PATCH /rounds/{id}POST /rounds/{id}/optimiseDELETE /rounds/{id}
If somebody changed the round since you read it, the call is refused with 409
and error: "stale", rather than quietly undoing their change. Read the round
again and decide. Without If-Match the call goes through as before.