@indiegems/gem-web-sdk 20.0.0 → 20.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/game.d.ts +51 -3
- package/dist/gem-web-sdk.bridge.js +2 -2
- package/dist/gem-web-sdk.conformance.js +3 -3
- package/dist/gem-web-sdk.esm.js +3 -3
- package/dist/gem-web-sdk.function.js +3 -3
- package/dist/gem-web-sdk.game.js +3 -3
- package/dist/gem-web-sdk.min.js +3 -3
- package/dist/gem-web-sdk.sdk.js +3 -3
- package/dist/gem-web-sdk.server.js +2 -2
- package/dist/index.d.ts +57 -6
- package/dist/sdk.d.ts +51 -3
- package/docs/api/api-reference-function.md +1 -1
- package/docs/api/api-reference-game.md +9 -8
- package/docs/api/api-reference-server.md +1 -1
- package/docs/api/reference.json +4 -4
- package/docs/changelog.md +14 -0
- package/docs/dedicated-server-flow.md +30 -2
- package/docs/getting-started.md +3 -3
- package/docs/manifest.json +14 -14
- package/docs/match-session.md +9 -0
- package/docs/p2p-and-parties.md +79 -6
- package/docs/server-bundles.md +2 -2
- package/docs/what-a-game-must-do.md +8 -5
- package/package.json +1 -1
- package/schema/bridge.v1.json +67 -2
- package/schema/capabilities.public.json +6 -0
package/dist/sdk.d.ts
CHANGED
|
@@ -2254,6 +2254,15 @@ interface GemSignalEvent {
|
|
|
2254
2254
|
readonly matchId: string;
|
|
2255
2255
|
readonly data: unknown;
|
|
2256
2256
|
}
|
|
2257
|
+
interface GemRelayDraining {
|
|
2258
|
+
readonly gem: typeof PROTOCOL;
|
|
2259
|
+
readonly type: "gem.relayDraining";
|
|
2260
|
+
readonly matchId: string;
|
|
2261
|
+
readonly clusterTargetId: string;
|
|
2262
|
+
readonly relayIps: string[];
|
|
2263
|
+
readonly drainingSince: string;
|
|
2264
|
+
readonly deadline: string;
|
|
2265
|
+
}
|
|
2257
2266
|
interface GemDedicatedEvent {
|
|
2258
2267
|
readonly gem: typeof PROTOCOL;
|
|
2259
2268
|
readonly type: "gem.dedicatedEvent";
|
|
@@ -2570,6 +2579,7 @@ interface TelemetrySample {
|
|
|
2570
2579
|
readonly type: string;
|
|
2571
2580
|
readonly value?: number;
|
|
2572
2581
|
readonly transport?: TransportLabel;
|
|
2582
|
+
readonly labels?: Readonly<Record<string, string>>;
|
|
2573
2583
|
}
|
|
2574
2584
|
interface P2pApiOptions {
|
|
2575
2585
|
readonly client: GemClient;
|
|
@@ -2686,9 +2696,12 @@ interface IceCandidateLike {
|
|
|
2686
2696
|
readonly sdpMid?: string | null;
|
|
2687
2697
|
readonly sdpMLineIndex?: number | null;
|
|
2688
2698
|
}
|
|
2699
|
+
type EventListenerLike = {
|
|
2700
|
+
bivarianceHack(event: unknown): void;
|
|
2701
|
+
}["bivarianceHack"];
|
|
2689
2702
|
interface EventTargetLike {
|
|
2690
|
-
addEventListener(type: string, listener:
|
|
2691
|
-
removeEventListener(type: string, listener:
|
|
2703
|
+
addEventListener(type: string, listener: EventListenerLike): void;
|
|
2704
|
+
removeEventListener(type: string, listener: EventListenerLike): void;
|
|
2692
2705
|
}
|
|
2693
2706
|
interface DataChannelLike extends EventTargetLike {
|
|
2694
2707
|
readonly readyState: string;
|
|
@@ -2703,6 +2716,16 @@ interface DataChannelInitLike {
|
|
|
2703
2716
|
readonly negotiated?: boolean;
|
|
2704
2717
|
readonly id?: number;
|
|
2705
2718
|
}
|
|
2719
|
+
interface IceConfigurationLike {
|
|
2720
|
+
iceServers?: {
|
|
2721
|
+
urls: string | string[];
|
|
2722
|
+
username?: string;
|
|
2723
|
+
credential?: string;
|
|
2724
|
+
}[];
|
|
2725
|
+
}
|
|
2726
|
+
interface StatsReportLike {
|
|
2727
|
+
forEach(callback: (value: unknown, key: string) => void): void;
|
|
2728
|
+
}
|
|
2706
2729
|
interface PeerConnectionLike extends EventTargetLike {
|
|
2707
2730
|
readonly localDescription: SessionDescriptionLike | null;
|
|
2708
2731
|
readonly remoteDescription: SessionDescriptionLike | null;
|
|
@@ -2715,6 +2738,10 @@ interface PeerConnectionLike extends EventTargetLike {
|
|
|
2715
2738
|
setRemoteDescription(description: SessionDescriptionLike): Promise<void>;
|
|
2716
2739
|
addIceCandidate(candidate: IceCandidateLike): Promise<void>;
|
|
2717
2740
|
close(): void;
|
|
2741
|
+
setConfiguration?(configuration: IceConfigurationLike): void;
|
|
2742
|
+
getConfiguration?(): IceConfigurationLike;
|
|
2743
|
+
restartIce?(): void;
|
|
2744
|
+
getStats?(): Promise<StatsReportLike>;
|
|
2718
2745
|
}
|
|
2719
2746
|
interface PeerConnectionConfig {
|
|
2720
2747
|
readonly iceServers: readonly {
|
|
@@ -2749,6 +2776,7 @@ interface P2pSessionOptions {
|
|
|
2749
2776
|
readonly baseUrl: string;
|
|
2750
2777
|
readonly signaling: SignalingClient;
|
|
2751
2778
|
readonly clusterTargetId?: ClusterTargetLike;
|
|
2779
|
+
readonly clusterTargetIds?: readonly string[];
|
|
2752
2780
|
readonly peerConnectionFactory?: PeerConnectionFactory;
|
|
2753
2781
|
readonly certificate?: unknown;
|
|
2754
2782
|
readonly connectTimeoutMs?: number;
|
|
@@ -2765,7 +2793,11 @@ declare class P2pSession {
|
|
|
2765
2793
|
private readonly keys;
|
|
2766
2794
|
private localPlayerId;
|
|
2767
2795
|
private migration;
|
|
2768
|
-
private
|
|
2796
|
+
private lease;
|
|
2797
|
+
private clusterTargetIds;
|
|
2798
|
+
private leaseRenewal;
|
|
2799
|
+
private leaseAskedAt;
|
|
2800
|
+
private leaseRenewalWarned;
|
|
2769
2801
|
private credential;
|
|
2770
2802
|
private nextPeerId;
|
|
2771
2803
|
private ended;
|
|
@@ -2779,6 +2811,7 @@ declare class P2pSession {
|
|
|
2779
2811
|
private readonly answeredSessions;
|
|
2780
2812
|
private credentialGeneration;
|
|
2781
2813
|
private credentialDeadline;
|
|
2814
|
+
private readonly drainTimers;
|
|
2782
2815
|
constructor(options: P2pSessionOptions);
|
|
2783
2816
|
get matchId(): string;
|
|
2784
2817
|
get playerId(): string;
|
|
@@ -2804,6 +2837,7 @@ declare class P2pSession {
|
|
|
2804
2837
|
private fetchCredential;
|
|
2805
2838
|
private waitForCredential;
|
|
2806
2839
|
private onOffer;
|
|
2840
|
+
private answer;
|
|
2807
2841
|
private onAnswer;
|
|
2808
2842
|
private hostVerified;
|
|
2809
2843
|
private maybeAuthenticate;
|
|
@@ -2814,6 +2848,17 @@ declare class P2pSession {
|
|
|
2814
2848
|
private watchMigration;
|
|
2815
2849
|
private onMatchEnded;
|
|
2816
2850
|
private onSignalingError;
|
|
2851
|
+
private onRelayDraining;
|
|
2852
|
+
private onDrainDeadline;
|
|
2853
|
+
private withoutDrainingRelays;
|
|
2854
|
+
private restartIceToHost;
|
|
2855
|
+
private offerIceRestart;
|
|
2856
|
+
private progressRestart;
|
|
2857
|
+
private reportRestart;
|
|
2858
|
+
private takeLease;
|
|
2859
|
+
private refreshLease;
|
|
2860
|
+
private reconfigureLinks;
|
|
2861
|
+
private iceServers;
|
|
2817
2862
|
private openLink;
|
|
2818
2863
|
private flushCandidates;
|
|
2819
2864
|
private onReliableOpen;
|
|
@@ -2822,6 +2867,8 @@ declare class P2pSession {
|
|
|
2822
2867
|
private refuseLink;
|
|
2823
2868
|
private finishAdmission;
|
|
2824
2869
|
private onIceState;
|
|
2870
|
+
private recoverHostLink;
|
|
2871
|
+
private voteHostUnreachable;
|
|
2825
2872
|
private onConnectionState;
|
|
2826
2873
|
private expose;
|
|
2827
2874
|
private dropLink;
|
|
@@ -4726,6 +4773,7 @@ interface FrameCallbacks {
|
|
|
4726
4773
|
onTicketEvent?(msg: GemTicketEvent): void;
|
|
4727
4774
|
onSocketState?(msg: GemSocketState): void;
|
|
4728
4775
|
onSignalEvent?(msg: GemSignalEvent): void;
|
|
4776
|
+
onRelayDraining?(msg: GemRelayDraining): void;
|
|
4729
4777
|
onDedicatedEvent?(msg: GemDedicatedEvent): void;
|
|
4730
4778
|
onClusterTarget?(msg: GemClusterTarget): void;
|
|
4731
4779
|
onPartyResult?(msg: GemPartyResult): void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# The function surface — API reference
|
|
2
2
|
|
|
3
|
-
Generated from `@indiegems/gem-web-sdk/function` v20.
|
|
3
|
+
Generated from `@indiegems/gem-web-sdk/function` v20.1.0, the version of the package this file ships in. Every symbol below carries an explicit anchor, its `anchor` in `reference.json` beside this file.
|
|
4
4
|
|
|
5
5
|
`@indiegems/gem-web-sdk/function` exports exactly the symbols on this page and nothing else.
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# The browser surface — API reference
|
|
2
2
|
|
|
3
|
-
Generated from `@indiegems/gem-web-sdk/sdk` v20.
|
|
3
|
+
Generated from `@indiegems/gem-web-sdk/sdk` v20.1.0, the version of the package this file ships in. Every symbol below carries an explicit anchor, its `anchor` in `reference.json` beside this file.
|
|
4
4
|
|
|
5
5
|
`@indiegems/gem-web-sdk/sdk` is the one import a game writes, wherever its code runs. `@indiegems/gem-web-sdk/game` exports exactly the symbols on this page and nothing else, for a build that wants only this half.
|
|
6
6
|
|
|
@@ -9504,8 +9504,9 @@ ARCADE_EMBEDDERS: readonly string[]
|
|
|
9504
9504
|
```
|
|
9505
9505
|
|
|
9506
9506
|
The origins the Gem Arcade embeds a game from, which every game accepts
|
|
9507
|
-
without naming them: the website
|
|
9508
|
-
|
|
9507
|
+
without naming them: the website and the Gem Arcade app. The list also
|
|
9508
|
+
holds `https://developer.gemarcade.com`, which is reserved and not live —
|
|
9509
|
+
no page is served there, so nothing frames a game from it.
|
|
9509
9510
|
|
|
9510
9511
|
You do not pass these. They are always allowed, and
|
|
9511
9512
|
[GemOptions.allowedEmbedders](#gemoptions-allowedembedders) adds to them rather than replacing them.
|
|
@@ -9534,8 +9535,8 @@ readonly allowedEmbedders?: readonly string[]
|
|
|
9534
9535
|
Extra origins allowed to embed your game, **added to** the ones the Gem
|
|
9535
9536
|
Arcade uses. Most games leave this out.
|
|
9536
9537
|
|
|
9537
|
-
The website
|
|
9538
|
-
|
|
9538
|
+
The website and the Gem Arcade app are always allowed — see
|
|
9539
|
+
[ARCADE_EMBEDDERS](#arcade_embedders) — so do not list them. Set this only when a site
|
|
9539
9540
|
of your own embeds your game.
|
|
9540
9541
|
|
|
9541
9542
|
```js
|
|
@@ -12593,9 +12594,9 @@ await gem.awaitBridge(); // before loading your main bundle
|
|
|
12593
12594
|
const me = await gem.awaitIdentity(); // who is playing, once sign-in completes
|
|
12594
12595
|
```
|
|
12595
12596
|
|
|
12596
|
-
No options are needed. The Gem Arcade's website
|
|
12597
|
-
|
|
12598
|
-
|
|
12597
|
+
No options are needed. The Gem Arcade's website and app are always allowed to
|
|
12598
|
+
embed your game ([ARCADE_EMBEDDERS](#arcade_embedders)), and the arcade tells your game
|
|
12599
|
+
which build it is.
|
|
12599
12600
|
|
|
12600
12601
|
A dev host on your own machine (`gemctl serve`, the Gem Editor) needs no
|
|
12601
12602
|
entry either: it frames a locally-served game from a loopback origin whose
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# The server surface — API reference
|
|
2
2
|
|
|
3
|
-
Generated from `@indiegems/gem-web-sdk/sdk` v20.
|
|
3
|
+
Generated from `@indiegems/gem-web-sdk/sdk` v20.1.0, the version of the package this file ships in. Every symbol below carries an explicit anchor, its `anchor` in `reference.json` beside this file.
|
|
4
4
|
|
|
5
5
|
`@indiegems/gem-web-sdk/sdk` is the one import a game writes, wherever its code runs. `@indiegems/gem-web-sdk/server` exports exactly the symbols on this page and nothing else, for a build that wants only this half.
|
|
6
6
|
|
package/docs/api/reference.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schema_version": 2,
|
|
3
3
|
"source": {
|
|
4
4
|
"repo": "IndieGems/gem-web-sdk",
|
|
5
|
-
"version": "20.
|
|
5
|
+
"version": "20.1.0"
|
|
6
6
|
},
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -11139,7 +11139,7 @@
|
|
|
11139
11139
|
"group": "start",
|
|
11140
11140
|
"signature": "ARCADE_EMBEDDERS: readonly string[]",
|
|
11141
11141
|
"documented": true,
|
|
11142
|
-
"summary": "The origins the Gem Arcade embeds a game from, which every game accepts\nwithout naming them: the website
|
|
11142
|
+
"summary": "The origins the Gem Arcade embeds a game from, which every game accepts\nwithout naming them: the website and the Gem Arcade app. The list also\nholds `https://developer.gemarcade.com`, which is reserved and not live —\nno page is served there, so nothing frames a game from it.\n\nYou do not pass these. They are always allowed, and\n[GemOptions.allowedEmbedders](api-reference-game.md#gemoptions-allowedembedders) adds to them rather than replacing them.",
|
|
11143
11143
|
"deprecated": null,
|
|
11144
11144
|
"params": [],
|
|
11145
11145
|
"returns": null,
|
|
@@ -11171,7 +11171,7 @@
|
|
|
11171
11171
|
"group": "start",
|
|
11172
11172
|
"signature": "readonly allowedEmbedders?: readonly string[]",
|
|
11173
11173
|
"documented": true,
|
|
11174
|
-
"summary": "Extra origins allowed to embed your game, **added to** the ones the Gem\nArcade uses. Most games leave this out.\n\nThe website
|
|
11174
|
+
"summary": "Extra origins allowed to embed your game, **added to** the ones the Gem\nArcade uses. Most games leave this out.\n\nThe website and the Gem Arcade app are always allowed — see\n[ARCADE_EMBEDDERS](api-reference-game.md#arcade_embedders) — so do not list them. Set this only when a site\nof your own embeds your game.\n\n```js\ncreateGem({ allowedEmbedders: ['https://my-own-site.example'] })\n```\n\nA page whose origin is not allowed gets no connection, and — deliberately —\nis told nothing about why. That means a typo here looks exactly like a\nhandshake that has not arrived: pass [GemOptions.onRefused](api-reference-game.md#gemoptions-onrefused) while you\nare integrating, because it is the only place the difference shows.\n\n⚠️ **Bake these in. Never build them from anything the page can\ninfluence** — not `location`, not a query parameter, not\n`document.referrer`. A value the embedder supplies is a check that can never\nfail. Do not add a `localhost:<port>` entry for your dev host either; the\nSDK admits one on your own machine by rule.",
|
|
11175
11175
|
"deprecated": null,
|
|
11176
11176
|
"params": [],
|
|
11177
11177
|
"returns": null,
|
|
@@ -13974,7 +13974,7 @@
|
|
|
13974
13974
|
"group": "start",
|
|
13975
13975
|
"signature": "function createGem(options?: GemOptions): Gem",
|
|
13976
13976
|
"documented": true,
|
|
13977
|
-
"summary": "Connect your game to the Gem Arcade. This is the one call every game makes.\n\nIt assembles everything and connects to the arcade that framed your game. The\nsame code runs on your machine and in production without a flag — but it needs\na frame either way. Opened at the top level it throws\n[GemUnframedError](api-reference-game.md#gemunframederror), which says how to run it: in the Arcade, the Editor,\nor the Gem dev host on your machine (`npm run dev`, signed in to `gemctl`).\n\nOne dependency, imported like anything else. The install is in\n[getting-started](../getting-started.md#3-add-the-sdk).\n\n```js\nimport { createGem } from '@indiegems/gem-web-sdk/sdk';\n\nconst gem = createGem();\n\nawait gem.awaitBridge(); // before loading your main bundle\nconst me = await gem.awaitIdentity(); // who is playing, once sign-in completes\n```\n\nNo options are needed. The Gem Arcade's website
|
|
13977
|
+
"summary": "Connect your game to the Gem Arcade. This is the one call every game makes.\n\nIt assembles everything and connects to the arcade that framed your game. The\nsame code runs on your machine and in production without a flag — but it needs\na frame either way. Opened at the top level it throws\n[GemUnframedError](api-reference-game.md#gemunframederror), which says how to run it: in the Arcade, the Editor,\nor the Gem dev host on your machine (`npm run dev`, signed in to `gemctl`).\n\nOne dependency, imported like anything else. The install is in\n[getting-started](../getting-started.md#3-add-the-sdk).\n\n```js\nimport { createGem } from '@indiegems/gem-web-sdk/sdk';\n\nconst gem = createGem();\n\nawait gem.awaitBridge(); // before loading your main bundle\nconst me = await gem.awaitIdentity(); // who is playing, once sign-in completes\n```\n\nNo options are needed. The Gem Arcade's website and app are always allowed to\nembed your game ([ARCADE_EMBEDDERS](api-reference-game.md#arcade_embedders)), and the arcade tells your game\nwhich build it is.\n\nA dev host on your own machine (`gemctl serve`, the Gem Editor) needs no\nentry either: it frames a locally-served game from a loopback origin whose\nport changes every run, and the SDK admits it by rule — only while the game\nitself is served from loopback, so nothing changes once you publish.\n\nPass `allowedEmbedders` only when a site of your own frames your game. It\nadds to the arcade's origins; see [GemOptions.allowedEmbedders](api-reference-game.md#gemoptions-allowedembedders).",
|
|
13978
13978
|
"deprecated": null,
|
|
13979
13979
|
"params": [
|
|
13980
13980
|
{
|
package/docs/changelog.md
CHANGED
|
@@ -16,6 +16,20 @@ Its entries are grouped under up to five headings:
|
|
|
16
16
|
- **Rebuild**: a type changed. Recompile against the new version; code that compiles today keeps compiling unless the entry says otherwise.
|
|
17
17
|
- **New capability**: something a game can now do. Nothing changes unless you use it.
|
|
18
18
|
|
|
19
|
+
## 20.1.0
|
|
20
|
+
|
|
21
|
+
Upgrading from 20.0.0. Impact: required.
|
|
22
|
+
|
|
23
|
+
### Rebuild
|
|
24
|
+
|
|
25
|
+
- The list of capability names a game can advertise gained one value, p2pRelayDrain. Recompile. If the game switches exhaustively over capability names, add a case for it. Otherwise nothing else changes.
|
|
26
|
+
- The list of messages the Arcade page sends to the game frame gained one entry, gem.relayDraining, and the parent message type gained the matching member. Recompile. If the game handles every parent message in an exhaustive switch, add a case for the new one. Otherwise nothing else changes.
|
|
27
|
+
|
|
28
|
+
### New capability
|
|
29
|
+
|
|
30
|
+
- A game can now be told when a relay carrying its peer-to-peer match is draining and by when. Pass onRelayDraining in the frame options, or implement it on the frame callbacks, to receive the notice. The SDK already moves the connection off the draining relay by itself, so use this only to inform the player or log it. The notice is sent only by an Arcade page that supports it.
|
|
31
|
+
- Telemetry samples can now carry labels, so a game that reads or forwards telemetry samples can see per-sample label values such as outcome and reason.
|
|
32
|
+
|
|
19
33
|
## 20.0.0
|
|
20
34
|
|
|
21
35
|
Upgrading from 19.0.2. Impact: required.
|
|
@@ -715,11 +715,39 @@ host does, and the SDK does it for you:
|
|
|
715
715
|
|
|
716
716
|
| What gem-api answers | Where | What the SDK does |
|
|
717
717
|
| --- | --- | --- |
|
|
718
|
-
| `invalid_cluster_target` | `create()
|
|
718
|
+
| `invalid_cluster_target` | `create()` | Creates in the next cluster the page sent, at once, until every one has been refused; then waits up to five seconds for the page to send a different list, and tries once more with a cluster it has not yet tried |
|
|
719
|
+
| `invalid_cluster_target` | `quickJoin()` | Waits up to five seconds for the page to send a different list, and tries once more with it |
|
|
719
720
|
| `invalid_cluster_target` | `startMatch()` — the platform does not run the room's cluster | Starts once in this player's best other cluster, naming it, which moves the room |
|
|
720
721
|
| `capacity_unavailable` twice in a row | `startMatch()` | Moves to the next acceptable cluster, once, and keeps waiting there |
|
|
721
722
|
|
|
722
|
-
**Each
|
|
723
|
+
**Each wait and each move happens once.** `create()` trying each cluster the page sent counts as one
|
|
724
|
+
step, and it tries three at most, because the page sends three at most. A refusal after that is the
|
|
725
|
+
answer, and it reaches you as the error.
|
|
726
|
+
|
|
727
|
+
### When the platform hides a cluster
|
|
728
|
+
|
|
729
|
+
An operator can **hide** a cluster to take it out of service without stopping it: it leaves the list
|
|
730
|
+
the page measures, it takes no new rooms, no new matches and no new TURN relay leases, and the
|
|
731
|
+
matches already running on it carry on. A page that measured before the hide still lists it until
|
|
732
|
+
it measures again, so the SDK is what moves that player on:
|
|
733
|
+
|
|
734
|
+
- **`create()`** is refused with `invalid_cluster_target` for the hidden cluster and creates in the
|
|
735
|
+
next one the page sent, with no wait.
|
|
736
|
+
- **`quickJoin()` and matchmaking** need nothing: the platform drops the hidden cluster from the list
|
|
737
|
+
they send and serves the rest.
|
|
738
|
+
- **`startMatch()`** in a room on the hidden cluster is refused the same way and starts once in the
|
|
739
|
+
host's best other cluster, which moves the room — as for any cluster the platform stopped running.
|
|
740
|
+
- **A P2P relay lease** is answered from another cluster in the same region when there is one, and
|
|
741
|
+
otherwise refused; the SDK then leases from the next cluster the page sent. See
|
|
742
|
+
`p2p-and-parties.md` §4.
|
|
743
|
+
|
|
744
|
+
An earlier SDK version reaches the same place more slowly or not at all. Its `create()` names only the
|
|
745
|
+
best cluster, waits up to five seconds for a list the page will not send, and then gives you the
|
|
746
|
+
refusal, so a game that creates rooms shows an error until the page measures again. Its P2P lease asks
|
|
747
|
+
the hidden cluster once and, with no other cluster in that region to answer for it, connects without
|
|
748
|
+
a relay. Its `startMatch()` and matchmaking behave as above, and so does its `quickJoin()` from
|
|
749
|
+
5.11.0 on, when the SDK began sending the page's whole list; before that a quick-join names one
|
|
750
|
+
cluster, and one naming only the hidden cluster is refused.
|
|
723
751
|
|
|
724
752
|
**An automatic start has nobody to answer, so the host's SDK answers it.** When a room's template
|
|
725
753
|
starts the match as it fills to `min_players` and the platform refuses that start, no call is waiting
|
package/docs/getting-started.md
CHANGED
|
@@ -50,7 +50,7 @@ either way.
|
|
|
50
50
|
```jsonc
|
|
51
51
|
// package.json
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@indiegems/gem-web-sdk": "20.
|
|
53
|
+
"@indiegems/gem-web-sdk": "20.1.0"
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -137,8 +137,8 @@ const me = await gem.awaitIdentity();
|
|
|
137
137
|
|
|
138
138
|
Two things about that call are worth getting right the first time.
|
|
139
139
|
|
|
140
|
-
**You do not list who may embed your game.** The Gem Arcade's website
|
|
141
|
-
|
|
140
|
+
**You do not list who may embed your game.** The Gem Arcade's website and app are always
|
|
141
|
+
allowed, and so is a dev host on your own machine (`gemctl serve`, the Gem Editor) while your
|
|
142
142
|
game is served from loopback. `allowedEmbedders` exists only for a page beyond those — see
|
|
143
143
|
[the embedder allowlist](what-a-game-must-do.md#1-the-embedder-allowlist) — and it adds to the
|
|
144
144
|
arcade's origins rather than replacing them.
|
package/docs/manifest.json
CHANGED
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
"file": "changelog.md",
|
|
51
|
-
"sha256": "
|
|
52
|
-
"updated": "2026-11-
|
|
51
|
+
"sha256": "8a292ee18fe7f910be0af049d96df66ccebf5c810fe5495c11d5352f7ee121f4",
|
|
52
|
+
"updated": "2026-11-03T00:39:38Z"
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
"file": "daily-challenges.md",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
60
|
"file": "dedicated-server-flow.md",
|
|
61
|
-
"sha256": "
|
|
62
|
-
"updated": "2026-10-
|
|
61
|
+
"sha256": "4a51622df43be68b5b9a88ae89907972090e64fd9b793ba74e5daa576489281c",
|
|
62
|
+
"updated": "2026-10-27T02:41:17Z"
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
65
|
"file": "deprecation-policy.md",
|
|
@@ -78,8 +78,8 @@
|
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
80
|
"file": "getting-started.md",
|
|
81
|
-
"sha256": "
|
|
82
|
-
"updated": "2026-11-
|
|
81
|
+
"sha256": "0c4cee83ffc77711f43ccdaafb394283a5a412b78395db1434ebbc2ab6943667",
|
|
82
|
+
"updated": "2026-11-03T00:39:38Z"
|
|
83
83
|
},
|
|
84
84
|
{
|
|
85
85
|
"file": "inventory-and-catalogs.md",
|
|
@@ -93,13 +93,13 @@
|
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
95
|
"file": "match-session.md",
|
|
96
|
-
"sha256": "
|
|
97
|
-
"updated": "2026-
|
|
96
|
+
"sha256": "121eb2bd92da9daf3922097699e0ab450da6a087eec8de552fe573094cf7e2c4",
|
|
97
|
+
"updated": "2026-11-02T16:12:53Z"
|
|
98
98
|
},
|
|
99
99
|
{
|
|
100
100
|
"file": "p2p-and-parties.md",
|
|
101
|
-
"sha256": "
|
|
102
|
-
"updated": "2026-
|
|
101
|
+
"sha256": "1de834a88746a4c7b6de555b3e6e3500fce0b7e75fc3cb87d46795efc7e27940",
|
|
102
|
+
"updated": "2026-11-02T23:33:48Z"
|
|
103
103
|
},
|
|
104
104
|
{
|
|
105
105
|
"file": "platform-moments.md",
|
|
@@ -138,8 +138,8 @@
|
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
"file": "server-bundles.md",
|
|
141
|
-
"sha256": "
|
|
142
|
-
"updated": "2026-11-
|
|
141
|
+
"sha256": "f54aeb88c268e5b657be9d0ea323d44cc61bfbb76fc84670f8a607d4514b005d",
|
|
142
|
+
"updated": "2026-11-03T00:39:38Z"
|
|
143
143
|
},
|
|
144
144
|
{
|
|
145
145
|
"file": "serverless-functions-js.md",
|
|
@@ -163,8 +163,8 @@
|
|
|
163
163
|
},
|
|
164
164
|
{
|
|
165
165
|
"file": "what-a-game-must-do.md",
|
|
166
|
-
"sha256": "
|
|
167
|
-
"updated": "2026-11-
|
|
166
|
+
"sha256": "059f3c8a31c0f6098d2c8e4836b79e866d0c5486443dc89039e160648946bd68",
|
|
167
|
+
"updated": "2026-11-02T23:27:22Z"
|
|
168
168
|
}
|
|
169
169
|
]
|
|
170
170
|
}
|
package/docs/match-session.md
CHANGED
|
@@ -155,6 +155,15 @@ own. When a host migration is already in flight the attempt waits it out instead
|
|
|
155
155
|
the way back, and it re-attaches this connection without a dial (§5.1 of
|
|
156
156
|
[p2p-and-parties.md](p2p-and-parties.md)).
|
|
157
157
|
|
|
158
|
+
**A relay that moves is not a drop.** When the platform takes a TURN relay out of service, or one
|
|
159
|
+
dies under a match, the SDK moves the peer-to-peer connection to another relay with an ICE restart
|
|
160
|
+
on the connection it already has (§4.1 and §5 of [p2p-and-parties.md](p2p-and-parties.md)). The
|
|
161
|
+
data channels stay open, so there is no `matchSuspended`, no `matchReconnected` and nothing to
|
|
162
|
+
resync — at most a pause of a few seconds while the route changes. Only a move that misses the
|
|
163
|
+
platform's deadline, or a restart that never comes back, leaves that path: the first is a drop with
|
|
164
|
+
`cause: 'link_closed'` and the re-dial above; the second is treated as the host being gone, as §5
|
|
165
|
+
there describes.
|
|
166
|
+
|
|
158
167
|
The loop never runs while the page is hidden. It parks, and the next visible edge — the phone
|
|
159
168
|
unlocked, the tab foregrounded — is itself a read, so a match that ended while the player was away
|
|
160
169
|
resolves to `matchEnded` and one that is still running resumes reconnecting. Nothing in your game
|
package/docs/p2p-and-parties.md
CHANGED
|
@@ -327,6 +327,14 @@ peer connection's configuration when it is constructed. Leasing afterwards
|
|
|
327
327
|
produces a connection that works whenever both peers are directly reachable —
|
|
328
328
|
which passes every test on a LAN and fails for a large minority of real players.
|
|
329
329
|
|
|
330
|
+
**A lease has a lifetime, and a match can outlast it.** The SDK keeps the
|
|
331
|
+
lease's expiry and renews it whenever it is about to build a connection on a
|
|
332
|
+
lease within two minutes of running out — a host answering a player's offer,
|
|
333
|
+
or a player rebuilding after a migration — so a host
|
|
334
|
+
deep into a long match never answers with credentials that have already
|
|
335
|
+
lapsed. A renewal also reaches every connection already up, ready for its next
|
|
336
|
+
change of route. Renewals are paced at one per event, never one per candidate.
|
|
337
|
+
|
|
330
338
|
**Which cluster to lease from is normally nobody's decision to make here.** The
|
|
331
339
|
parent measures the regions and hands the close ones across the bridge, and each
|
|
332
340
|
peer leases from its own best one — a relay near that peer, whichever cluster
|
|
@@ -343,6 +351,23 @@ alike — which is a decision to move the room. When nothing resolves one the
|
|
|
343
351
|
connection proceeds without a relay and says so loudly, because the failure it
|
|
344
352
|
causes is the one that looks like success.
|
|
345
353
|
|
|
354
|
+
**A lease refused for its cluster moves to the next one the page sent.** The page
|
|
355
|
+
sends up to three clusters, best first. When the platform refuses a lease because
|
|
356
|
+
of the cluster it named — `cluster_hidden`, an operator took it out of service and
|
|
357
|
+
no other cluster in its region can relay for it, or `invalid_cluster_target`, it
|
|
358
|
+
is not one the platform runs — the SDK leases from the next, before any connection
|
|
359
|
+
is built. Only when every one is refused does the connection proceed without a
|
|
360
|
+
relay, with the same warning. Any other refusal is not walked, because another
|
|
361
|
+
cluster would answer it the same. A cluster you named yourself is the only one
|
|
362
|
+
tried. A renewal walks the same way, starting from the cluster that granted the
|
|
363
|
+
lease it replaces, and a re-dial after a dropped connection leases afresh over the
|
|
364
|
+
whole list, so a cluster hidden mid-match is passed over either way.
|
|
365
|
+
|
|
366
|
+
An earlier SDK version asks only the best cluster. Where the platform can answer
|
|
367
|
+
from another cluster in the same region it does, and that SDK gets its relay
|
|
368
|
+
anyway; where it cannot, that SDK connects without one. `dedicated-server-flow.md`
|
|
369
|
+
§5 says what hiding a cluster does to rooms and matches.
|
|
370
|
+
|
|
346
371
|
**`null` names no cluster, exactly as omitting the option does.** A game picking
|
|
347
372
|
its target out of a list writes `targets[0]?.id ?? null`, so that is the spelling
|
|
348
373
|
ordinary code arrives with — and it defers to the parent's answer the same way an
|
|
@@ -351,6 +376,41 @@ request for a cluster with no id: that costs the caller the no-relay warning and
|
|
|
351
376
|
reports "the relay was refused" for a player whose actual problem is that nobody
|
|
352
377
|
ever resolved a region for them.
|
|
353
378
|
|
|
379
|
+
### 4.1 A relay going away does not end the match
|
|
380
|
+
|
|
381
|
+
Relays are taken out of service for deploys, credential rotation and scaling.
|
|
382
|
+
The platform does it in three ordered steps:
|
|
383
|
+
|
|
384
|
+
1. **Delist.** No new lease names the relay, so no new connection is built on it.
|
|
385
|
+
2. **Move.** Every player in a match that may be using it is told, and the
|
|
386
|
+
connections on it move elsewhere while it still forwards traffic.
|
|
387
|
+
3. **Stop.** The relay stops once nobody is on it, or when the platform's
|
|
388
|
+
deadline for the move passes.
|
|
389
|
+
|
|
390
|
+
**Your game sees none of it.** On the notice every player renews its lease and
|
|
391
|
+
drops the draining relay from the list it holds. A player whose connection to
|
|
392
|
+
the host runs through that relay moves with an **ICE restart on the connection
|
|
393
|
+
it already has**: new candidates on the remaining relays, the same DTLS
|
|
394
|
+
session, the same three data channels. The host answers on its existing
|
|
395
|
+
connection. Nothing closes, `gem.match` raises no `matchSuspended`, and nobody
|
|
396
|
+
reports the host unreachable. A player whose route does not touch the draining
|
|
397
|
+
relay renews and does nothing else, and the host renews and answers but never
|
|
398
|
+
offers a restart of its own.
|
|
399
|
+
|
|
400
|
+
**If the move has not completed by the platform's deadline**, the player drops
|
|
401
|
+
that connection and the ordinary reconnect takes over (§5.1, and §4 of
|
|
402
|
+
[match-session.md](match-session.md)): a fresh session at the same host, over
|
|
403
|
+
the renewed lease, reported as this player's own link closing — `link_closed` —
|
|
404
|
+
because the host is fine. The same happens at once if the host has already let
|
|
405
|
+
go of that player's connection when the restart reaches it: the host can only
|
|
406
|
+
answer from a new connection, which the player's existing one cannot use.
|
|
407
|
+
|
|
408
|
+
**The announced move needs the Arcade page to pass the notice on.** The page is
|
|
409
|
+
what relays it into the game frame, and a page whose side of the bridge
|
|
410
|
+
predates the SDK release that brought this drops it. Until the page is at that
|
|
411
|
+
release or later, a relay stopping under a match is recovered the way an
|
|
412
|
+
unannounced loss is (§5): a few seconds of quiet, then the same restart.
|
|
413
|
+
|
|
354
414
|
---
|
|
355
415
|
|
|
356
416
|
## 5. Host migration is where a split match can happen
|
|
@@ -388,18 +448,31 @@ trying to fix, and the old host is already gone. A host that settled on its own
|
|
|
388
448
|
send could not tell that apart from a healthy match. The active phase is bounded
|
|
389
449
|
at 30 seconds and a stall is reported as `migration_failed`.
|
|
390
450
|
|
|
391
|
-
A non-host
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
451
|
+
A non-host whose connection to the host goes quiet **tries to bring it back
|
|
452
|
+
before it reports anything**. ICE `disconnected` is what a lost relay looks like
|
|
453
|
+
as much as a lost host, so the player first renews its lease and restarts ICE on
|
|
454
|
+
the existing connection — the move §4.1 describes, without the notice — and
|
|
455
|
+
gives it about eight seconds to come back. Only if it does not, or the
|
|
456
|
+
connection fails or closes outright meanwhile, does the player report the host
|
|
457
|
+
unreachable, **once per epoch**. A relay that dies without warning therefore no
|
|
458
|
+
longer ends a two-player match or migrates a larger one away from a healthy
|
|
459
|
+
host; the cost is that a host who really has gone is reported a few seconds
|
|
460
|
+
later.
|
|
461
|
+
|
|
462
|
+
The report is a vote, not a verdict. In a match of three or more, gem-api
|
|
463
|
+
migrates on a quorum of at least two independent reporters, so one player
|
|
464
|
+
cannot unseat a host who is fine for everyone else. In a two-player match there
|
|
465
|
+
is nobody to elect, and the lone peer's report ends the match with `host_left`
|
|
466
|
+
— which is why the report waits for the restart to fail.
|
|
395
467
|
|
|
396
468
|
### 5.1 What `gem.match` does with a migration
|
|
397
469
|
|
|
398
470
|
A `MatchConnection` to a hosting peer survives a migration as a **pause**: its
|
|
399
471
|
channels re-resolve on `hostChanged`, so the game sees a gap rather than a new
|
|
400
472
|
connection to build. What the room reports meanwhile is the ordinary session
|
|
401
|
-
vocabulary — `matchSuspended` with cause `
|
|
402
|
-
link
|
|
473
|
+
vocabulary — `matchSuspended` with cause `link_closed` when this side sees the
|
|
474
|
+
host's link fail or `host_unreachable` when the platform reports the host gone,
|
|
475
|
+
`matchReconnected` with `attempt: 0` when the migration seated a
|
|
403
476
|
new host and the link came back without a re-dial, and `matchEnded` with
|
|
404
477
|
`host_left` or `migration_failed` when it did not. The screens for each are
|
|
405
478
|
[match-session.md](match-session.md) §3 and §4; the listen host's own
|
package/docs/server-bundles.md
CHANGED
|
@@ -80,8 +80,8 @@ To check the download yourself: the package npm serves is, byte for byte, the fi
|
|
|
80
80
|
also publishes on its download host, with the hash beside it:
|
|
81
81
|
|
|
82
82
|
```bash
|
|
83
|
-
curl -fsSL -O https://downloads.gemarcade.com/sdk/v20.
|
|
84
|
-
curl -fsSL https://downloads.gemarcade.com/sdk/v20.
|
|
83
|
+
curl -fsSL -O https://downloads.gemarcade.com/sdk/v20.1.0/gem-web-sdk.tgz
|
|
84
|
+
curl -fsSL https://downloads.gemarcade.com/sdk/v20.1.0/gem-web-sdk.tgz.sri
|
|
85
85
|
openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
|
|
86
86
|
```
|
|
87
87
|
|
|
@@ -53,10 +53,10 @@ No `baseUrl`. The Arcade names the backend on every token and the SDK routes the
|
|
|
53
53
|
tokens only if its origin is allowed, and the pages that matter are always allowed without a word
|
|
54
54
|
from you:
|
|
55
55
|
|
|
56
|
-
- **the Gem Arcade website
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
- **the Gem Arcade website and app** — the SDK carries their origins as `ARCADE_EMBEDDERS`, on every
|
|
57
|
+
platform the app runs on. The list also holds `https://developer.gemarcade.com`, which is
|
|
58
|
+
reserved and not live: no page is served there. You play your own dev and test builds with
|
|
59
|
+
`gemctl serve` or the Gem Editor, or on your game's dev or test channel in the Arcade;
|
|
60
60
|
- **a dev host on your own machine** — `gemctl serve` and the Gem Editor frame a locally-served game
|
|
61
61
|
from a loopback origin whose port changes every run, and the SDK admits that shape by rule, only
|
|
62
62
|
while your game itself is served from loopback. Once published, nothing about the rule applies.
|
|
@@ -1710,7 +1710,10 @@ stops your media elements, and restarts neither. The snippet above has stopped l
|
|
|
1710
1710
|
the SDK does this part for you. After the page was hidden, or a context left `running` without your
|
|
1711
1711
|
asking, it listens again for the player's next lift in your frame, resumes the contexts that were
|
|
1712
1712
|
running and replays the elements that were playing when the sound was cut off, then stops
|
|
1713
|
-
listening.
|
|
1713
|
+
listening. A context that reads `running` after the page comes back can still be silent, as when
|
|
1714
|
+
your own `resume()` ran outside a tap, so on that first lift the SDK also suspends and resumes each
|
|
1715
|
+
context that had run, once per return: your clock pauses for a moment, and the sound comes back.
|
|
1716
|
+
You write nothing for it. It stays out of your way:
|
|
1714
1717
|
|
|
1715
1718
|
- **A context you `suspend()` yourself is left alone** until you call `resume()`. If you pause your
|
|
1716
1719
|
audio when the page is hidden and resume it when it comes back, keep doing that: the SDK steps in
|