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 —
HereandAnnounce, the platform primitives under the seats - topic — how a topic's address and
Candidateswork - ConcurrencyCheck — the refusal the sentence explains
- Changes — the facts
MovedFirstreads kits/presence/tests— the kit's front door: staff-only seats, who is stamped, the refusal through a screen