Skip to content

slate 0.0.29 — base64url, and a source that is told

Choose a tag to compare

@edadma edadma released this 05 Sep 12:20
· 79 commits to dev since this release

Two things a framework asked for, and neither could be had from outside the compiler: a base64url
a program can reach, and a streamed response that tells its source the reader has gone.

base64url in slate:url

import { base64urlEncode, base64urlDecode } from slate:url

val cookie = base64urlEncode(hmac("sha256", secret, payload))
val r = base64urlDecode(fromTheClient)

RFC 4648 §5's alphabet — base64 with - for + and _ for /and no padding at all, a =
being exactly what the alphabet exists to avoid.

Encode takes a string or an array of bytes, and a string is its UTF-8 bytes: the same reading
encodeComponent gives one beside it.

Decode answers a result whose value is BYTES. Text encoded this way arrives from outside — a
cookie, a token, a signature somebody sent — so being malformed is a condition the caller was always
going to deal with rather than a fault. And what was encoded is as likely to be a digest as a
sentence, so answering text would be guessing; fromBytes is one call and answers a result of its
own, so the two compose.

Three things are refused, each with its own sentence: a character outside the alphabet, =
included; a length one past a multiple of four, which carries no whole byte in its last character
and so is truncated rather than merely odd; and bits past the last whole byte that are not zero,
or two spellings would decode to the same bytes — which is how a signature check is walked past in a
format that compares its tokens as text.

slate:jwt had the only encoder there was and it was private. Three places in
sluice wanted one and wrote hex instead, paying a third
more bytes on a cookie that is already percent-encoded. That module now imports the shared pair; the
algorithm is the one it carried.

slate:url still asks nothing of the host, so this works under the interpreter, under node and in a
browser with no branch anywhere.

A streamed source is told when its reader has gone

A source may have a close, and the server calls it where a streamed response ends with the source
unexhausted
— the client hung up, the socket was closed under the response, the peer reset the
stream, or the source itself faulted. A source that ran to done is told nothing, having finished.

subscribe(topic)
    async pull()
        { done: false, value: await take(topic) }

    shut()
        forget(topic)

    { next: pull, close: shut }

app.get("/events", req -> sse(subscribe("orders")))

It is optional and is asked for exactly as next is, so every source already written keeps
working: a generator has no close, and neither has an object that does not name one.

Until now a writer that stopped pulling said nothing at all, and a source is usually a
subscription to something — a topic, a query, the tail of a file. sluice's event hub is the case that
named it: close() was the handler's to call from a place that could not tell the client had gone,
so a subscriber nobody closed stayed on its topic for the life of the program, one per browser tab
ever opened. Its subscribe already answers { next, close, dropped }, so an event stream behind it
stops leaking with nothing written there at all.

Both writers do it, and HTTP/2 has two ways out that HTTP/1.1 has not — a session that went away
under the response, and a peer that reset this one stream while the connection carries on. sse
forwards the message to the source it was given, without which the protocol would stop at the
wrapper. It is called once, whichever way the response ended, so a source counting its own
readers cannot go wrong.

Also in this release

docs/library/url.md documents the pair and docs/library/http.md the source protocol, both run by
the suite as usual. tests/js/p24.sl joins the differential corpus, so base64url is pinned against
RFC 4648's published vectors on both back ends rather than against a round trip that could be wrong
twice.