puter.peer: room names, relayed guest grants, servers that come back
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s

A peer server could only be reached by the invite code the signaller
minted for it, which is unknowable ahead of time and dies with the
server's socket. Any app that wanted a link to share in advance or reuse
had to run a registry of its own that maps a stable code to whatever
invite is live — and keep it live, since the signaller socket can drop
without the library saying a word (`onclose` was an empty function),
leaving a host serving an invite nobody can dial.

`serve({ name })` serves under a room name of the caller's choosing;
`connect(name)` dials it. A name is held by whoever serves it and free
once they stop; serving a held name from another identity rejects with
`name_in_use`, from the same identity it takes over. A connection to a
room nobody serves fails with `no_host`, so a lobby has a definite answer
to poll on instead of guessing between "not yet" and "gone".

`serve({ guestGrant })` and `server.setGuestGrant()` leave a guest grant
with the signaller, which hands it to every anonymous guest in the connect
reply; the connection redeems it before making its offer, so a guest with
no session gets relays without the app publishing the grant anywhere.

A server whose signaller socket drops now re-registers on its own with
backoff — under the same name, or a fresh code announced by the new
`reconnect` event — and pings the socket to keep idle proxies from cutting
it. It fires `close` (`replaced`, `name_in_use`) when it has to stand
down for good. Existing connections are peer-to-peer and never affected.

Signaller refusals reach the connection's `error` event as an Error with
a `code` next to the message. Keepalive replies and stray frames no longer
throw in the message handlers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jelveh
2026-08-25 13:24:45 -07:00
co-authored by Claude Fable 5
parent 2a45a10854
commit e006b19773
8 changed files with 878 additions and 76 deletions
+5 -1
View File
@@ -13,6 +13,10 @@ The `PuterPeerConnection` object representing a WebRTC data-channel connection t
Information about the user who created the server, with `username` and `uuid`.
#### `room` (String)
The room name this connection was made in, when the server was reached by name (see the `name` option of [`puter.peer.serve()`](/Peer/serve/)). `undefined` for a connection made on an invite code.
#### `connected` (Boolean)
Whether the data channel is currently open.
@@ -51,7 +55,7 @@ Fired when the connection closes. `event.reason` holds the reason, if one was gi
#### `error`
Fired when a connection error occurs. `event.error` holds the error.
Fired when a connection error occurs. `event.error` holds the error. When the signaller refused the connection it is an `Error` whose `code` says why — `no_host` (a room nobody is serving right now), `invalid_invite` (an invite code that is not live) or `invalid_auth` — and `close` follows.
## Example
+18 -2
View File
@@ -11,7 +11,7 @@ The `PuterPeerServer` object returned by [`puter.peer.serve()`](/Peer/serve/). I
#### `inviteCode` (String)
The code to share with other clients so they can connect with [`puter.peer.connect()`](/Peer/connect/).
The code to share with other clients so they can connect with [`puter.peer.connect()`](/Peer/connect/). For a server started with a `name`, this is the name. For one on a generated code, it can change if the server has to re-register with the signaller — see the `reconnect` event.
#### `connections` (Map)
@@ -21,7 +21,11 @@ A `Map` of every connected client, keyed by connection id. The values are [`Pute
#### `close()`
Closes every client connection and the signalling connection. The invite code stops working.
Closes every client connection and the signalling connection. The invite code stops working; a room name is free for someone else to serve.
#### `setGuestGrant(grant)`
Replaces the guest grant handed to clients that connect from now on (see the `guestGrant` option of [`puter.peer.serve()`](/Peer/serve/)). Grants expire, so a long-running host issues a fresh one with [`puter.peer.createGuestGrant()`](/Peer/createGuestGrant/) before the old one lapses and passes it here. Pass `null` to stop handing one out.
## Events
@@ -32,6 +36,18 @@ Fired when a client connects. The event has the following attributes:
- `conn` ([`PuterPeerConnection`](/Objects/puterpeerconnection/)) - The connection to the client.
- `user` (Object) - Metadata about the connecting user, with `username` and `uuid` (if available).
#### `reconnect`
Fired when the server has re-registered with the signaller after losing its connection to it. Nothing about existing client connections changes; this only concerns clients yet to connect. The event has:
- `inviteCode` (String) - The invite code in force now. Unchanged for a server with a `name`; a server on a generated code gets a fresh one, since the old one died with the connection — share the new one.
#### `close`
Fired when the server has stopped accepting clients without `close()` having been called. Existing connections stay open; the invite code no longer works. The event has:
- `reason` (String) - `replaced` when a newer server of yours took the same room name over (the same account, or the same `anonToken`, called `serve()` again with that name), or `name_in_use` when the name is held by someone else and could not be reclaimed after the connection was lost.
## Example
```js
+3 -1
View File
@@ -10,10 +10,12 @@ Use the Peer API to build peer-to-peer applications without the need for a serve
<div class="info">
Hosting a session requires authentication — on websites, Puter.js will prompt the user if needed. Guests can join without an account: pass `anonToken`, plus a `turnGrant` from the host so the connection can still use Puter's relays. See [`puter.peer.createGuestGrant()`](/Peer/createGuestGrant/).
Hosting a session requires authentication — on websites, Puter.js will prompt the user if needed. Guests can join without an account: pass `anonToken`, plus a `turnGrant` from the host so the connection can still use Puter's relays — or let the host serve with a `guestGrant`, which reaches guests on its own. See [`puter.peer.createGuestGrant()`](/Peer/createGuestGrant/).
</div>
A server is reached either by the invite code it was handed — good for as long as it serves — or by a **room name** of your choosing (`puter.peer.serve({ name: 'friday-standup' })`), which anyone can dial with `puter.peer.connect('friday-standup')` for as long as someone serves it. Room names are how you make a link that can be shared ahead of time and reused; see [`puter.peer.serve()`](/Peer/serve/#room-names).
## Features
#### Create a peer server and exchange messages
+8 -2
View File
@@ -24,7 +24,7 @@ const conn = await puter.peer.connect(inviteCode, options);
#### `inviteCode` (required)
A string invite code created by `puter.peer.serve()`.
The invite code a `puter.peer.serve()` call was given, or the **room name** it was started with (`serve({ name })`). The two are told apart by shape — generated codes are uppercase (`NJ-7F3A9C`), room names lowercase — so pass whichever you were handed.
#### `options` (optional)
@@ -33,12 +33,18 @@ A string invite code created by `puter.peer.serve()`.
- `iceServers` (`RTCIceServer[]`) Custom ICE servers (STUN/TURN) to use instead of the Puter-managed relays.
- `forceRelay` (`boolean`) Whether to force connections to route through a relay instead of attempting peer-to-peer (default). Metering charges may apply.
- `anonToken` (`String`) Join without a Puter session. Any uuid — it identifies this guest for the duration of the session, and no sign-in prompt is shown. The host sees the guest as `anonymous`, so anything you want to call them is yours to send over the connection.
- `turnGrant` (`String`) A grant from [`puter.peer.createGuestGrant()`](/Peer/createGuestGrant/). Lets a guest use the Puter-managed relays on the host's account. Without one, a guest connects only where a direct connection is possible; with `forceRelay`, a guest needs one.
- `turnGrant` (`String`) A grant from [`puter.peer.createGuestGrant()`](/Peer/createGuestGrant/). Lets a guest use the Puter-managed relays on the host's account. Without one, a guest still gets relays when the host serves with a `guestGrant` — that grant reaches the guest through the signaller. Otherwise a guest connects only where a direct connection is possible; with `forceRelay`, a guest needs a grant one way or the other.
## Return value
A `Promise` that resolves to a [`PuterPeerConnection`](/Objects/puterpeerconnection/) instance, which carries `send()` and `close()` methods and the `open`, `message`, `close`, and `error` events.
The promise resolves once the connection has been requested, not once it is open — wait for `open` before sending. If the signaller refuses the connection, the instance fires `error` with an `Error` whose `code` says why, then `close`:
- `no_host` — the room name is valid but nobody is serving it right now. Try again in a few seconds; the host may be on their way.
- `invalid_invite` — the invite code is not live: it was mistyped, or the server that issued it is gone.
- `invalid_auth` — the session token or `anonToken` was not accepted.
## Example
```html
+1 -1
View File
@@ -5,7 +5,7 @@ platforms: [websites, apps]
---
Creates a **guest grant**: a short-lived token that lets people without a Puter session use the Puter-managed TURN relays. Hand it to the people you invite alongside the invite code, and they pass it to [`puter.peer.connect()`](/Peer/connect/) as `turnGrant`.
Creates a **guest grant**: a short-lived token that lets people without a Puter session use the Puter-managed TURN relays. Either hand it to the people you invite alongside the invite code, and they pass it to [`puter.peer.connect()`](/Peer/connect/) as `turnGrant` — or pass it to [`puter.peer.serve()`](/Peer/serve/) as `guestGrant`, and every guest that connects without a grant of their own receives it through the signaller.
Without a grant, a guest can still join a session — but only over direct connections. Relay credentials are what make a connection work when one side is behind a NAT or firewall that blocks direct traffic, and minting them requires an account. The grant is how your account vouches for the guest.
+19
View File
@@ -29,6 +29,8 @@ const server = await puter.peer.serve(options);
- `iceServers` (`RTCIceServer[]`) Custom ICE servers (STUN/TURN) to use instead of the Puter-managed relays.
- `forceRelay` (`boolean`) Whether to force connections to route through a relay instead of attempting peer-to-peer (default). Metering charges will increase.
- `anonToken` (`String`) Host without a Puter session. Any uuid; no sign-in prompt is shown. An anonymous host has no account to attribute relay usage to, so it cannot issue guest grants and gets no relays of its own.
- `name` (`String`) Serve under a **room name** of your choosing instead of a generated invite code. Clients connect with the same string: `puter.peer.connect(name)`. See [Room names](#room-names) below.
- `guestGrant` (`String`) A grant from [`puter.peer.createGuestGrant()`](/Peer/createGuestGrant/) to hand to guests. Every client that connects with `anonToken` and no `turnGrant` of its own receives it through the signaller and uses the relays on your account, so you never have to deliver the grant some other way. Renew it with [`server.setGuestGrant()`](/Objects/puterpeerserver/#setguestgrant-grant) before it expires.
To let people join your session without accounts of their own, keep hosting authenticated and give them a grant — see [`puter.peer.createGuestGrant()`](/Peer/createGuestGrant/).
@@ -36,6 +38,23 @@ To let people join your session without accounts of their own, keep hosting auth
A `Promise` that resolves to a [`PuterPeerServer`](/Objects/puterpeerserver/) instance, which carries the `inviteCode` to share, the `connections` map of connected clients, and a `connection` event fired as each client joins.
Rejects with an `Error` whose `code` is `name_in_use` when `name` is currently being served by someone else, and with a `TypeError` when `name` is not a valid room name.
## Room names
A generated invite code (`NJ-7F3A9C`) is minted when you call `serve()` and stops working when the server goes away — good for a one-off session, useless for a link you want to share ahead of time or reuse. A room name is an address you pick, and it is the same every time you serve it:
```js
const server = await puter.peer.serve({ name: 'friday-standup' });
server.inviteCode; // 'friday-standup'
```
- Names are 3–64 characters of lowercase letters, digits and hyphens, not starting or ending with a hyphen.
- A name is held by whoever is serving it right now, first come, and is free again the moment that server stops. While it is held, `serve()` from **another** identity rejects with `name_in_use`. From the **same** identity — the same account, or the same `anonToken` — the newer server takes the name over and the older one fires `close` with reason `replaced`, so a host whose connection dropped can come straight back, and a user who opens the same room twice ends up with the newest tab serving it.
- A client that connects to a room nobody is serving gets a definite answer — an `error` event whose `code` is `no_host` — so a lobby can simply try again in a few seconds until the host arrives.
A server stays reachable on its own: if its connection to the signaller drops, it re-registers under the same name (or, for a generated code, under a fresh one, announced by the `reconnect` event). Existing connections are never affected by this — they are peer-to-peer.
## Example
```html