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.