Skip to content

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.

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.

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.

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.

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}/optimise
  • DELETE /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.