Summary#
An Osy# app runs in two places. The server holds the data, the secrets and the authority to enforce security. The browser holds the screen, the router and the person using it. A single function body can need both, and the runtime moves between them as the body runs.
Description#
A call is a call#
Calling something that runs in the other place is written exactly like calling anything else. You write
Login(email, password), and the platform evaluates the arguments where you are, suspends, runs the body where the
work is, and resumes you with the result.
That includes failure. An exception raised over there arrives here as an exception, so a try/catch wrapped around
a crossing behaves the way the same code would if nothing had crossed at all:
try {
var token = Login(email, password); // the body runs on the server
Navigation.Go("/home"); // this line is back in the browser
} catch (Exception e) {
error = e.Message; // and so is this
}One body, both places#
A body is not confined to one place. It can read and validate on the server and then ask the person something in the browser, in one straight line, and the runtime hands each part to whichever engine has what it needs:
[Route("/orders")]
component OrderPage() {
action Cancel(Order order) {
if (Confirm(order)) { // ↩ reads and validates on the server…
Navigation.Go("/orders"); // …and this line comes back to the browser
}
}
}What crosses arrives unchanged#
A value that crosses is the same value. A decimal keeps every digit and does not become a floating-point
approximation; a long above 2^53 survives intact; a DateTime keeps its instant. A comparison that is true on one
side is true on the other, and a total adds up to the same number wherever it was summed. Text behaves identically
down to the awkward cases — Text.Upper treats the German ß the same in both engines.
So a crossing is not a conversion, and nothing needs to be re-checked on arrival.
Where the data is#
The data store lives on the server, so a query over an entity reaches the server to answer:
var c = Customer.Single(x => x.Email == email); // the rows are on the serverA query over a list you already hold is not a data read at all — the list is already here, and filtering it is ordinary work on a value in hand:
int Cheap(List<Line> lines) {
return lines.Where(l => l.Price < 10).Count();
}The standard library works the same way: Text.Upper, Text.Trim, s.Length, name.Contains("x") and the
arithmetic operate on a value you are already holding, so they happen in place. A handful of calls need something
only the server has — Security.HashPassword needs the host's salt generator, Security.IssueJwt its signing key,
Crypto.Encrypt a key the browser must never hold — and those reach the server for it. See The standard library — and where each call runs for the
full table of which is which.
What a crossing costs#
Time, and nothing else. Work that happens in place costs nothing; a crossing is one round trip, and a body that crosses several times pays for several. That is the whole of the difference — the answers are identical either way, so the cost is a latency question and never a correctness one.
Examples#
string Initials(string first, string last) {
return first.Substring(0, 1) + last.Substring(0, 1);
}entity Customer { string Email; string Name; }
string NameFor(string email) {
return Customer.Single(c => c.Email == email).Name;
}See also#
- function — declaring a function
- LINQ over a local list — LINQ over a list you already hold
- component — a component, whose actions begin in the browser
- The standard library — and where each call runs — every standard-library call, and where it runs