marslang rs-0.19.1

std.broadcast#

Named channels that programs send messages on and listen to: within one program, between the programs of one user on one computer, or across the local network. Receiving is a task to await, so a program waits for messages without holding up its other tasks (see async).

mars
takepkg std.broadcast;
takepkg std.async;

func m{
    news = broadcast.channel("scores", "local");
    func listen(){ out(await news.receive()); }
    t = async.spawn(listen);
    news.send(map().set("player", "Ada").set("points", 10));
    await t;                                   // {"player": "Ada", "points": 10}
}

A channel object is made for a name and a scope, and from that moment it both sends and receives: every message sent on its name and scope reaches it, its own included. A message is any value JSON can hold, and every receiver gets a copy of its own.

Scope Who sends and receives Carried by
"local" This program only Memory
"machine" Programs of the same user on this computer; no other user Unix domain sockets in a directory only the user can open
"network" Anyone on the local network UDP broadcast
Function Result
channel(name, scope) A Channel for name in scope
network_port(name) The UDP port of the network channel name, from 41000 to 48999
machine_directory() The directory where machine channels register
Member of a Channel Result
send(value) Sends value to every receiver: how many it reached for "local" and "machine", null for "network"
receive() A task giving the next message
receive_for(seconds) A task giving the next message, or null if none comes within seconds, a number or a duration
on(f) Calls f(message), as a task, for each message; returns a Subscription
close() Stops listening; true, or false when already closed
name, scope What it was made with
registration For "machine", the socket file that registers it; otherwise null
closed Whether close() was called

A Subscription has stop(), which returns true, or false when it was already stopped, and active.

Names are 1 to 100 letters, digits, -, _, and ., as in build.jobs or sensor-3. The scope is always written out: no scope is assumed, and the network one is never chosen for you.

Receiving#

receive() gives the oldest message waiting for that channel object, or waits for the next one; only the task that awaits it waits. Messages that arrive while nothing is receiving are queued per channel object, up to 1000; beyond that the oldest is dropped. Since a channel object listens from the moment it is made, a message sent before anything awaits receive() is waiting for it, as in the first example.

mars
takepkg std.broadcast;
takepkg std.async;

async func later(any ch){
    await async.sleep(0.05);
    ch.send("ready");
}

func m{
    jobs = broadcast.channel("jobs", "local");
    workers = broadcast.channel("jobs", "local");
    out(jobs.send(1), jobs.send(2));                 // 2 2
    out(await workers.receive(), await workers.receive());   // 1 2

    out(await workers.receive_for(0.01));           // null
    later(jobs);
    out(await workers.receive_for(1));              // ready
}

Messages from one sender arrive in the order it sent them. receive_for gives null when time runs out, so a message that is null itself looks the same; await receive() with async.timeout to tell them apart. A message that arrives just as the time runs out is given, not dropped.

What receive gives is the value itself for "local" and "machine", whose senders are this program or this user's. For "network" it is a map that also says who sent it, since anyone may have:

text
{"message": {"temperature": 21}, "from": "192.168.1.5"}

Subscriptions#

on(f) calls f(message) for every message from then on, each call a task of its own, so a slow handler holds up nothing; f may be an async function. While a channel object has a subscription, its messages go to its subscriptions, not to receive(). An error in a handler fails the program when it ends, as for any task nothing awaited.

mars
takepkg std.broadcast;
takepkg std.async;

func m{
    alerts = broadcast.channel("alerts", "local");
    sub = alerts.on(func(any message) => out("alert:", message));
    alerts.send("disk full");                       // alert: disk full
    await async.yield_now();
    sub.stop();
    alerts.send("ignored");
    out(await alerts.receive());                    // ignored
}

Does a subscription keep the program running? A "machine" or "network" subscription does, as an open server does in Node.js: a program whose m ends with one active keeps receiving until it is stopped or its channel is closed, which a handler may do. A "local" subscription does not: only the program itself can send on a local channel, so once its code and tasks are done no message can come, and the program ends.

mars
takepkg std.broadcast;

func m{
    commands = broadcast.channel("commands", "machine");
    sub = null;
    func obey(any command){
        out("doing", command);                      // doing stop
        if (command == "stop"){ sub.stop(); }
    }
    sub = commands.on(obey);
    commands.send("stop");                          // another program could send this
}

Machine channels#

A "machine" channel reaches every program of the same user on this computer that has a channel object for its name, and no program of any other user: that is a hard rule, enforced by the operating system.

Only the current user. On Linux the directory is $XDG_RUNTIME_DIR/marslang-broadcast-UID, a directory of the user's own, or else /tmp/marslang-broadcast-UID (macOS: in $TMPDIR). It is made with permissions 0700, and it is refused unless it is a real directory, not a link, that the user owns; if its permissions are wider they are narrowed to 0700. Each socket file is 0600. Connecting to a socket needs search permission on its directory, so no other user can send, and no other user can listen, since listening means making a file there. On Windows (10 and later, which have AF_UNIX sockets) the directory is %TEMP%\marslang-broadcast, in the user's profile. Each time a program first uses it, it is checked to be owned by the user, and given an access list, not inherited from its parent, with one entry: full control for the user, which the socket files inherit. Administrators, and root on Unix, can open anything, these files included; they are outside what any program can keep out.

mars
takepkg std.broadcast;

func m{
    builds = broadcast.channel("build.done", "machine");
    out(builds.send(map().set("ok", true)));        // 1, or more with other listeners
    out(await builds.receive());                    // {"ok": true}
    out(builds.registration != null);               // true
    builds.close();
}

Network channels#

Anyone on the local network can send and read network messages. They are not encrypted or authenticated, and nothing stops a stranger sending one that looks like yours, so do not use them for secrets or to decide anything that matters without checking. Use them for things like finding other programs, or announcing what is not private.

mars
takepkg std.broadcast;

func m{
    hello = broadcast.channel("hello", "network");
    out(broadcast.network_port("hello"));           // 46672
    out(hello.send("anyone there?"));               // null
    reply = await hello.receive_for(1);             // our own message comes back
    out(reply.get("message"));                      // anyone there?
    hello.close();
}

Closing#

close() stops a channel object listening: its queued messages are dropped, its subscriptions stop, a receive waiting on it raises ClosedError, and a machine registration is removed. Using a closed channel raises ClosedError. Channel objects close by themselves when the program ends, but a program that makes many should close the ones it is done with: each local one keeps receiving copies, and each machine or network one a socket and a thread.

Errors#

Error When
RangeError A name that is not 1 to 100 letters, digits, -, _, or .; a scope other than "local", "machine", or "network"; a message over the scope's limit; a negative number of seconds
TypeError A value JSON cannot hold, such as a set, a pair, a function, or a map with keys that are not strings
broadcast.ClosedError The channel object was closed; inherits from Error
Error The machine directory is not safe to use, or a socket or thread could not be made
CancelledError The task awaiting receive or receive_for was cancelled; no message is lost
mars
takepkg std.broadcast;

func m{
    run { broadcast.channel("two words", "local"); } handle(RangeError e){
        out(e.message);   // broadcast.channel needs a name of 1 to 100 letters, digits, '-', '_', or '.', got "two words"
    }
    run { broadcast.channel("news", "everywhere"); } handle(RangeError e){
        out(e.message);   // broadcast.channel needs the scope "local", "machine", or "network", got "everywhere"
    }
    run { broadcast.channel("news", "local").send(set(1)); } handle(TypeError e){
        out(e.message);   // send needs a value JSON can hold: JSON has no sets; write an array of the items instead
    }
}

How it works#

Channels, the local scope, the queues of local channel objects, copying (each receiver parses the JSON text that send wrote once), receive_for, and subscriptions are Marslang (std/broadcast.mars). Native (std/rs/broadcast.rs) are only what Marslang cannot do:

A machine subscription keeps the program running because a task waits for its messages on a worker thread, which the event loop waits for; a local one waits for nothing, so none is left once the program is done.