REST and deferred close
The same orchestrator over plain HTTP, plus a toggle that lets a client finish a round on its own schedule.
REST transport
binaryTransport stays the default: a slot is a long-lived
session with many small messages, and a WebSocket carrying msgpack fits
that. REST is for the cases where it does not - a back-office tool, a
smoke test, a curl in a runbook, or a client behind something that will
not proxy WebSockets.
import { createServer, restTransport, loadTsMath, cryptoRng } from "@open-rgs/core";
await createServer({
manifest,
platform,
transport: restTransport({ port: 8080 }),
}); POST /session { "sid": "..." }
POST /spin { "sid": "...", "betIndex": 0 }
POST /round/open { "sid": "...", "mode": "deferred", "betIndex": 0 }
POST /round/step { "sid": "...", "action": { "type": "gamble" } }
POST /round/end { "sid": "..." }
POST /promo/accept { "sid": "...", "accept": true }
GET /healthz REST cannot push. A wallet balance change, or the
supersede frame that makes concurrencyPolicy: "kick-old"
work, has nowhere to go. closeConnection is deliberately not
implemented, so the policy degrades to "allow" and
createServer warns at boot rather than doing nothing quietly.
Each response carries the current balance instead.
There is also no connection to bind a session to, so the sid
in the body is the only credential. That is the same token the operator's
launch URL carries, but REST leans on it harder than the WebSocket
transport does - do not put this transport somewhere the sid is casually
visible.
Deferred close
A simple round opens and closes in one call: spin computes
the outcome, settles the money, and the round is over before the response
is written. If the player closes the tab mid-presentation there is nothing
to come back to - the balance moved, with no way to see why.
import { withDeferredClose } from "@open-rgs/core";
const spin = await loadTsMath("./maths/spin.ts", { rng: cryptoRng });
modes: {
default: { math: withDeferredClose(spin, { prompt: "Collect" }), stakeMultiplier: 1 },
} This turns the game into a complex round, and it is worth
being plain about that rather than presenting it as a third round shape.
open runs the simple math and parks the outcome;
close pays it. The money moves twice, exactly like any other
complex round. The adapter's job is to make that conversion a one-line
toggle instead of a rewrite.
Two consequences you now own. An open round holds an outstanding debit, so
a player who never returns leaves it open until something closes it -
which is what autoclose is for, and it matters more with this
toggle on. And spin is refused while a round is open, so a
stuck round blocks play until it settles.
Coming back to it
Re-initialising a session with an unfinished round resumes it. The outcome
was fixed when the round opened, so ops replays the spin that
already happened and the only thing left is to close it. The orchestrator
flags that, so a client need not know which modes defer their close.
{
"balance": 98000,
"resume": {
"roundId": "...",
"ops": [ { "kind": "spin", "mult": 2 } ],
"awaiting": { "type": "endRound", "prompt": "Collect" },
"replay": {
"unfinished": true,
"message": "Unfinished round, watching replay"
}
}
}
A late close pays exactly what the open decided. A player who never came
back is not penalised, and one who came back late is not rewarded -
autoclose settles at the same value for the same reason.