Osy#betaRun it on Osyrin: osyrin.com

Reference / Realtime

The Presence kit — who else is on a record, and who moved it first

use Osysharp.Presence@0; · policy CollaboratesOnRecords => IsStaff; · RecordSeats(id) · MovedFirst(Case.Changes(id))

`Osysharp.Presence` shows who else has a record open right now — any record, addressed by its id — with what they are doing ("viewing", "replying", "assigning") and never what they wrote; and it turns a concurrency refusal into the sentence a person needs: "Ada changed Amount 2 minutes ago — your edit is still here."

preview1 example compiled by CIrealtimekitpresenceconcurrency

Source on GitHub: Osysharp.Presence — github.com/osysharp/presence ↗

Summary#

use Osysharp.Presence@0; gives every record in an app a seat set: open the record and you are in it, close it and you are gone — the platform keeps it from the connection and stamps who sits in each seat. A seat carries an activity word and never content. The app says one thing — who may see colleagues on a record:

policy CollaboratesOnRecords => IsStaff;

Signature#

policy CollaboratesOnRecords;                                // the app supplies it
public class RecordSeat { Principal? Who; string Activity; } // Who is stamped by the platform
topic RecordPresence(Guid recordId) { Candidates = u => CollaboratesOnRecords; Carries = RecordSeat; Presence = true; }

void SeatActivity(Guid recordId, string activity);           // say what you are doing on it now
List<RecordSeat> Colleagues(List<RecordSeat> here, Guid me); // everyone but the viewer
string WhoIsHere(List<RecordSeat> colleagues);               // "Raj is replying" · "Raj and Ines are here"
component RecordSeats(Guid recordId, string activity = "viewing");   // the header strip; announces the viewer
component RecordSeatsLine(Guid recordId);                            // one line for a list row; announces nothing

string MovedFirst(List<EntityChange> changes, string otherwise = "Somebody changed this while you had it open", DateTime? now = null);
string TookFirst(string who, DateTime at, string what = "took this", DateTime? now = null);
string Ago(DateTime at, DateTime now);                        // "just now" · "12 minutes ago" · "on 4 Oct at 14:05"

Description#

The record is the room#

RecordPresence is addressed by a record's id, so the kit works for any entity — a ticket, a case, an approval — without knowing which. Being subscribed IS having the record open: there is nothing to post and nothing to clean up, and a crashed browser cannot linger. RecordSeats(id) subscribes and announces the viewer as "viewing" (or the activity you pass) when it mounts; SeatActivity(id, "replying") changes what you are doing later, from a composer or a menu.

Staff only, by the app's rule#

The topic's door is CollaboratesOnRecords. Anyone it refuses cannot subscribe, and announcing without a seat puts nobody in the set — Announce decorates your own seat and never creates one. A customer looking at their own ticket is never shown in, or shown, the desk's seats unless the app's policy says so.

Who is stamped; content never travels#

RecordSeat.Who is filled in by the platform from the connection; new RecordSeat { Who = … } is refused at compile. And a seat has no field for words: what somebody wrote reaches an open page through the row's own read rules (SECURITY-INVARIANT realtime-broadcast-signal-only), so a note written by a colleague cannot reach anyone through presence.

The sentence that names who moved it first#

[ConcurrencyCheck] refuses a save over somebody else's newer one. MovedFirst turns the facts <Entity>.Changes(id) hands over — who, when, which fields, never values — into the sentence: "Ada changed Amount and Owner 2 minutes ago". Make it on the server (the trail is the server's) and add the half only the page knows: "— your edit is still here." TookFirst(who, at) says the same for an ownership collision: "Ines took this 2 minutes ago".

Online is not available#

A seat says who is looking. It is not consent to receive work: a routing kit reads its own explicit availability, never this topic, because a held socket is not activity and an open tab is not a yes.

Examples#

A case editor with seats and the refusal's sentence#

app CaseDesk {
  use Osysharp.Identity;
  use Osysharp.Presence;
  use Osysharp.Observability;
  use Osysharp.Ui;
  model "model/**/*.osy";
}

using Osysharp.Identity;
using Osysharp.Presence;
using Osysharp.Observability;

[Role] enum DeskRole { Authenticator, Staff }
policy IsStaff => RoleGrant.Any(g => g.Member == user && g.Role == DeskRole.Staff);
policy CollaboratesOnRecords => IsStaff;

[ConcurrencyCheck]
entity Case {
  [Required, MaxLength(40)] string Reference;
  decimal Amount;
  security { allow read, create, update when IsStaff; }
}

[Route("/case/{id}")]
component CaseEditor(Guid id) {
  var cases = Case.Where(c => c.Id == id).ToList();
  var message = "";
  action Save(Case c) {
    try { c.Amount = c.Amount + 1; UnitOfWork.Commit(); message = "Saved."; }
    catch (ConflictException e) { message = WhoMovedIt(c.Id) + " — your edit is still here."; }
  }
  render {
    Stack(gap: 3, p: 4) {
      RecordSeats(id);
      if (message != "") { Alert(message, tone: Tone.Warning); }
      foreach (var c in cases) {
        Row(gap: 3, align: Align.Center) {
          Text(c.Reference);
          Button(label: "Add one", onPress: () => Save(c));
        }
      }
    }
  }
}

string WhoMovedIt(Guid id) { return MovedFirst(Case.Changes(id)); }

See also#

  • Here, Announce — Here and Announce, the platform primitives under the seats
  • topic — how a topic's address and Candidates work
  • ConcurrencyCheck — the refusal the sentence explains
  • Changes — the facts MovedFirst reads
  • kits/presence/tests — the kit's front door: staff-only seats, who is stamped, the refusal through a screen

Related

Here, Announce

Who is currently on a presence topic, and what they say they are doing. Here is the converging set of people present…

topic

A topic is a named, addressable realtime destination: a declared place to put a message so that every open page…

ConcurrencyCheck

Plain optimistic concurrency: refuses an update whose value was changed by somebody else after the value the write was…

Changes

The lightweight, ungated answer to "who changed this row, and when": field NAMES only, never values, newest first…

The Conversations kit — one timeline per conversation, across every channel

`Osysharp.Conversations` keeps a conversation with a customer as ONE ordered timeline — their messages, the team's…