@indiegems/gem-web-sdk 19.0.2 → 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/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: (event: never) => void): void;
2691
- removeEventListener(type: string, listener: (event: never) => void): void;
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 iceServers;
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` v19.0.2, the version of the package this file ships in. Every symbol below carries an explicit anchor, its `anchor` in `reference.json` beside this file.
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` v19.0.2, the version of the package this file ships in. Every symbol below carries an explicit anchor, its `anchor` in `reference.json` beside this file.
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
 
@@ -4779,16 +4779,17 @@ purchase(skuId: string): Promise<ArcadeOutcome>
4779
4779
  Ask the arcade to open checkout for one SKU from [StoreApi.offers](#storeapi-offers):
4780
4780
  the payment provider's page on the web, the app store's purchase sheet in
4781
4781
  the Arcade's apps, straight away, with no confirmation screen of the
4782
- arcade's own first. **Call it from the player's press handler** — the
4783
- arcade refuses an ask no person pressed for, `declined` / `busy`.
4782
+ arcade's own first. It needs no press: call it from your Buy button's
4783
+ handler, or after an `await` in it, and the checkout opens either way.
4784
4784
 
4785
4785
  - `ok` — the player paid. Re-read balances and inventory through `gem.me`;
4786
4786
  nothing here carries an amount. With `reason: 'pending'`, the provider
4787
4787
  has not settled yet: [StoreApi.onSettled](#storeapi-onsettled) hears when it does.
4788
4788
  - `declined` — nothing was bought: `cancelled` (the player closed the
4789
- checkout, or the offer cannot be bought again yet) or `busy` (after a
4790
- closed checkout the arcade refuses the next ask for a while). Return to
4791
- your screen silently, and do not ask again unprompted.
4789
+ checkout, or the offer cannot be bought again yet) or `busy` (another
4790
+ checkout or Arcade sheet was already open). Return to your screen
4791
+ silently, and do not ask again unprompted: the player's next press opens
4792
+ the checkout again, however many times they closed it.
4792
4793
  - `unavailable` with `store_off`, `sign_in_required`, `not_found`,
4793
4794
  `unsupported`, `invalid_argument` or a payment provider's failure code —
4794
4795
  nothing was bought.
@@ -4819,7 +4820,7 @@ Ask the arcade to show one SKU from [StoreApi.offers](#storeapi-offers) in its o
4819
4820
  sheet, over your paused and hidden game: its art, what the player gets, a
4820
4821
  sale if one is on, and, for a pass the player already holds, how long it
4821
4822
  has left. The sheet's Buy opens the same checkout as
4822
- [StoreApi.purchase](#storeapi-purchase). **Call it from the player's press handler.**
4823
+ [StoreApi.purchase](#storeapi-purchase). Like `purchase`, it needs no press.
4823
4824
 
4824
4825
  Use it for an offer you would have to hardcode a pitch for: VIP and other
4825
4826
  passes, and bundles. Your button only needs to say what it opens. Draw a
@@ -4828,8 +4829,8 @@ Gem pack yourself and use `purchase` instead. Check
4828
4829
 
4829
4830
  It answers exactly as `purchase` does, with one addition: `declined` /
4830
4831
  `cancelled` also covers the player leaving the sheet without pressing Buy,
4831
- and you are not told which of the two they closed. That starts the same
4832
- back-off, so do not ask again unprompted. `unavailable` / `unsupported`
4832
+ and you are not told which of the two they closed. Do not ask again
4833
+ unprompted; the player's next press opens it again. `unavailable` / `unsupported`
4833
4834
  means this Arcade does not draw the sheet, and nothing was sent.
4834
4835
  `unavailable` / `error` means **the outcome is unknown**: re-read balances
4835
4836
  and inventory through `gem.me`, and keep listening with
@@ -9503,8 +9504,9 @@ ARCADE_EMBEDDERS: readonly string[]
9503
9504
  ```
9504
9505
 
9505
9506
  The origins the Gem Arcade embeds a game from, which every game accepts
9506
- without naming them: the website, the developer site, and the Gem Arcade
9507
- app.
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.
9508
9510
 
9509
9511
  You do not pass these. They are always allowed, and
9510
9512
  [GemOptions.allowedEmbedders](#gemoptions-allowedembedders) adds to them rather than replacing them.
@@ -9533,8 +9535,8 @@ readonly allowedEmbedders?: readonly string[]
9533
9535
  Extra origins allowed to embed your game, **added to** the ones the Gem
9534
9536
  Arcade uses. Most games leave this out.
9535
9537
 
9536
- The website, the developer site and the Gem Arcade app are always
9537
- allowed — see [ARCADE_EMBEDDERS](#arcade_embedders) — so do not list them. Set this only when a site
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
9538
9540
  of your own embeds your game.
9539
9541
 
9540
9542
  ```js
@@ -11581,7 +11583,7 @@ sheet. To spend Gems or Coins on your game's own items, use
11581
11583
  [Gem.purchases](#gem-purchases).
11582
11584
 
11583
11585
  List what the store sells here with `offers()`, show each `priceDisplay`
11584
- exactly as given, and call `purchase(skuId)` from the player's press. For a
11586
+ exactly as given, and call `purchase(skuId)` when the player chooses to buy. For a
11585
11587
  pass, a bundle or anything else whose pitch you would have to hardcode, call
11586
11588
  `showOffer(skuId)` instead, and the arcade draws the offer. After `ok`,
11587
11589
  re-read balances through [Gem.me](#gem-me), and `gem.me.passes` for a pass.
@@ -12592,9 +12594,9 @@ await gem.awaitBridge(); // before loading your main bundle
12592
12594
  const me = await gem.awaitIdentity(); // who is playing, once sign-in completes
12593
12595
  ```
12594
12596
 
12595
- No options are needed. The Gem Arcade's website, developer site and app are
12596
- always allowed to embed your game ([ARCADE_EMBEDDERS](#arcade_embedders)), and the arcade
12597
- tells your game which build it is.
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.
12598
12600
 
12599
12601
  A dev host on your own machine (`gemctl serve`, the Gem Editor) needs no
12600
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` v19.0.2, the version of the package this file ships in. Every symbol below carries an explicit anchor, its `anchor` in `reference.json` beside this file.
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
 
@@ -2,7 +2,7 @@
2
2
  "schema_version": 2,
3
3
  "source": {
4
4
  "repo": "IndieGems/gem-web-sdk",
5
- "version": "19.0.2"
5
+ "version": "20.1.0"
6
6
  },
7
7
  "entries": [
8
8
  {
@@ -5735,7 +5735,7 @@
5735
5735
  "group": "store",
5736
5736
  "signature": "purchase(skuId: string): Promise<ArcadeOutcome>",
5737
5737
  "documented": true,
5738
- "summary": "Ask the arcade to open checkout for one SKU from [StoreApi.offers](api-reference-game.md#storeapi-offers):\nthe payment provider's page on the web, the app store's purchase sheet in\nthe Arcade's apps, straight away, with no confirmation screen of the\narcade's own first. **Call it from the player's press handler** — the\narcade refuses an ask no person pressed for, `declined` / `busy`.\n\n- `ok` — the player paid. Re-read balances and inventory through `gem.me`;\n nothing here carries an amount. With `reason: 'pending'`, the provider\n has not settled yet: [StoreApi.onSettled](api-reference-game.md#storeapi-onsettled) hears when it does.\n- `declined` — nothing was bought: `cancelled` (the player closed the\n checkout, or the offer cannot be bought again yet) or `busy` (after a\n closed checkout the arcade refuses the next ask for a while). Return to\n your screen silently, and do not ask again unprompted.\n- `unavailable` with `store_off`, `sign_in_required`, `not_found`,\n `unsupported`, `invalid_argument` or a payment provider's failure code —\n nothing was bought.\n- `unavailable` / `error` — **the outcome is unknown, and the player may\n have paid.** It is what you get when the bridge closes while checkout\n is open, or the arcade has no reliable answer. Never tell the player\n nothing was bought: re-read balances and inventory through `gem.me`,\n and keep listening with [StoreApi.onSettled](api-reference-game.md#storeapi-onsettled).\n\nNO TIMEOUT: it waits for as long as the arcade is connected, because a\npayment takes as long as it takes.",
5738
+ "summary": "Ask the arcade to open checkout for one SKU from [StoreApi.offers](api-reference-game.md#storeapi-offers):\nthe payment provider's page on the web, the app store's purchase sheet in\nthe Arcade's apps, straight away, with no confirmation screen of the\narcade's own first. It needs no press: call it from your Buy button's\nhandler, or after an `await` in it, and the checkout opens either way.\n\n- `ok` — the player paid. Re-read balances and inventory through `gem.me`;\n nothing here carries an amount. With `reason: 'pending'`, the provider\n has not settled yet: [StoreApi.onSettled](api-reference-game.md#storeapi-onsettled) hears when it does.\n- `declined` — nothing was bought: `cancelled` (the player closed the\n checkout, or the offer cannot be bought again yet) or `busy` (another\n checkout or Arcade sheet was already open). Return to your screen\n silently, and do not ask again unprompted: the player's next press opens\n the checkout again, however many times they closed it.\n- `unavailable` with `store_off`, `sign_in_required`, `not_found`,\n `unsupported`, `invalid_argument` or a payment provider's failure code —\n nothing was bought.\n- `unavailable` / `error` — **the outcome is unknown, and the player may\n have paid.** It is what you get when the bridge closes while checkout\n is open, or the arcade has no reliable answer. Never tell the player\n nothing was bought: re-read balances and inventory through `gem.me`,\n and keep listening with [StoreApi.onSettled](api-reference-game.md#storeapi-onsettled).\n\nNO TIMEOUT: it waits for as long as the arcade is connected, because a\npayment takes as long as it takes.",
5739
5739
  "deprecated": null,
5740
5740
  "params": [
5741
5741
  {
@@ -5758,7 +5758,7 @@
5758
5758
  "group": "store",
5759
5759
  "signature": "showOffer(skuId: string): Promise<ArcadeOutcome>",
5760
5760
  "documented": true,
5761
- "summary": "Ask the arcade to show one SKU from [StoreApi.offers](api-reference-game.md#storeapi-offers) in its own\nsheet, over your paused and hidden game: its art, what the player gets, a\nsale if one is on, and, for a pass the player already holds, how long it\nhas left. The sheet's Buy opens the same checkout as\n[StoreApi.purchase](api-reference-game.md#storeapi-purchase). **Call it from the player's press handler.**\n\nUse it for an offer you would have to hardcode a pitch for: VIP and other\npasses, and bundles. Your button only needs to say what it opens. Draw a\nGem pack yourself and use `purchase` instead. Check\n[StoreApi.showOfferAvailable](api-reference-game.md#storeapi-showofferavailable) first, and that the SKU is in `offers`.\n\nIt answers exactly as `purchase` does, with one addition: `declined` /\n`cancelled` also covers the player leaving the sheet without pressing Buy,\nand you are not told which of the two they closed. That starts the same\nback-off, so do not ask again unprompted. `unavailable` / `unsupported`\nmeans this Arcade does not draw the sheet, and nothing was sent.\n`unavailable` / `error` means **the outcome is unknown**: re-read balances\nand inventory through `gem.me`, and keep listening with\n[StoreApi.onSettled](api-reference-game.md#storeapi-onsettled).\n\nNO TIMEOUT: it waits for as long as the arcade is connected, because a\nplayer may read a sheet and pay for as long as they like.",
5761
+ "summary": "Ask the arcade to show one SKU from [StoreApi.offers](api-reference-game.md#storeapi-offers) in its own\nsheet, over your paused and hidden game: its art, what the player gets, a\nsale if one is on, and, for a pass the player already holds, how long it\nhas left. The sheet's Buy opens the same checkout as\n[StoreApi.purchase](api-reference-game.md#storeapi-purchase). Like `purchase`, it needs no press.\n\nUse it for an offer you would have to hardcode a pitch for: VIP and other\npasses, and bundles. Your button only needs to say what it opens. Draw a\nGem pack yourself and use `purchase` instead. Check\n[StoreApi.showOfferAvailable](api-reference-game.md#storeapi-showofferavailable) first, and that the SKU is in `offers`.\n\nIt answers exactly as `purchase` does, with one addition: `declined` /\n`cancelled` also covers the player leaving the sheet without pressing Buy,\nand you are not told which of the two they closed. Do not ask again\nunprompted; the player's next press opens it again. `unavailable` / `unsupported`\nmeans this Arcade does not draw the sheet, and nothing was sent.\n`unavailable` / `error` means **the outcome is unknown**: re-read balances\nand inventory through `gem.me`, and keep listening with\n[StoreApi.onSettled](api-reference-game.md#storeapi-onsettled).\n\nNO TIMEOUT: it waits for as long as the arcade is connected, because a\nplayer may read a sheet and pay for as long as they like.",
5762
5762
  "deprecated": null,
5763
5763
  "params": [
5764
5764
  {
@@ -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, the developer site, and the Gem Arcade\napp.\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.",
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, the developer site and the Gem Arcade app are always\nallowed — see [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.",
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,
@@ -13228,7 +13228,7 @@
13228
13228
  "group": "start",
13229
13229
  "signature": "readonly store: StoreApi",
13230
13230
  "documented": true,
13231
- "summary": "**Real money.** The Arcade Store's own Gem packs, Boosts, passes and\nbundles, sold for the player's money through the arcade's own purchase\nsheet. To spend Gems or Coins on your game's own items, use\n[Gem.purchases](api-reference-game.md#gem-purchases).\n\nList what the store sells here with `offers()`, show each `priceDisplay`\nexactly as given, and call `purchase(skuId)` from the player's press. For a\npass, a bundle or anything else whose pitch you would have to hardcode, call\n`showOffer(skuId)` instead, and the arcade draws the offer. After `ok`,\nre-read balances through [Gem.me](api-reference-game.md#gem-me), and `gem.me.passes` for a pass.\nNothing rejects.",
13231
+ "summary": "**Real money.** The Arcade Store's own Gem packs, Boosts, passes and\nbundles, sold for the player's money through the arcade's own purchase\nsheet. To spend Gems or Coins on your game's own items, use\n[Gem.purchases](api-reference-game.md#gem-purchases).\n\nList what the store sells here with `offers()`, show each `priceDisplay`\nexactly as given, and call `purchase(skuId)` when the player chooses to buy. For a\npass, a bundle or anything else whose pitch you would have to hardcode, call\n`showOffer(skuId)` instead, and the arcade draws the offer. After `ok`,\nre-read balances through [Gem.me](api-reference-game.md#gem-me), and `gem.me.passes` for a pass.\nNothing rejects.",
13232
13232
  "deprecated": null,
13233
13233
  "params": [],
13234
13234
  "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, developer site and app are\nalways allowed to embed your game ([ARCADE_EMBEDDERS](api-reference-game.md#arcade_embedders)), and the arcade\ntells your game which 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).",
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
  {
@@ -33,7 +33,7 @@ itself. You get an outcome.
33
33
  - [1. Two surfaces, and which one is real money](#1-two-surfaces-and-which-one-is-real-money)
34
34
  - [2. Nothing rejects](#2-nothing-rejects)
35
35
  - [3. What is available here](#3-what-is-available-here)
36
- - [4. Call the acting ones from a press](#4-call-the-acting-ones-from-a-press)
36
+ - [4. Leaving takes a press; buying does not](#4-leaving-takes-a-press-buying-does-not)
37
37
  - [5. App links](#5-app-links)
38
38
  - [6. Opening another game](#6-opening-another-game)
39
39
  - [7. Similar games, as cards](#7-similar-games-as-cards)
@@ -88,20 +88,25 @@ the bridge is up. A feature can be off for reasons you
88
88
  cannot see from inside the frame: a TV, an in-app browser that cannot take payment, an app build
89
89
  where it is withheld. Do not guess; read it.
90
90
 
91
- ## 4. Call the acting ones from a press
91
+ ## 4. Leaving takes a press; buying does not
92
92
 
93
93
  Four calls put something in front of the player: `gem.arcade.openApp`, `gem.arcade.openGame`,
94
- `gem.store.purchase` and `gem.store.showOffer`. **Call each one from the player's own press handler**, in the same turn as the
95
- press. The Arcade checks that a person pressed, and refuses an ask nobody pressed for with
96
- `declined` / `busy`, showing nothing.
97
-
94
+ `gem.store.purchase` and `gem.store.showOffer`.
95
+
96
+ - **Leaving the game takes a press.** Call `openApp` and `openGame` from the player's own press
97
+ handler, in the same turn as the press. The Arcade checks that a person pressed, and refuses an
98
+ ask nobody pressed for with `declined` / `busy`, showing nothing. Never on a timer, never on
99
+ load, never in a retry loop.
100
+ - **Buying does not.** `gem.store.purchase` and `gem.store.showOffer` open their checkout or sheet
101
+ after an `await`, from a timer, or where the browser cannot report a press: the checkout is where
102
+ the player confirms, and your game cannot press it. Nothing backs off either, so the player's
103
+ next press after closing a checkout opens it again. Still ask only because the player chose to
104
+ buy.
98
105
  - **Controllers count.** A gamepad press the Arcade sees counts as a press. Do not add a keyboard or
99
106
  pointer workaround for pad players.
100
107
  - **One at a time.** Only one of these is open at once, across both surfaces and the Arcade's other
101
108
  sheets (a Gem purchase, a share, a friend challenge). A second ask while one is open is
102
109
  `declined` / `busy`. The SDK answers it for you without sending.
103
- - **Never on a timer, never on load, never in a retry loop.** None of them would have a press, so
104
- all of them would be refused.
105
110
 
106
111
  The reads (`appLinks`, `similarGames`, `offers`) need no press. A second call while one is open
107
112
  shares the first one's answer.
@@ -250,9 +255,10 @@ first: the payment provider's page on the web, the app store's own purchase shee
250
255
  apps. That checkout is where the player confirms and pays. Your game is resumed before the answer
251
256
  arrives, so your own celebration is visible.
252
257
 
253
- - **Call it from the press** ([§4](#4-call-the-acting-ones-from-a-press)). The player's press on
254
- your Buy button is what opens checkout, so make that button say plainly what it buys and for how
255
- much (`priceDisplay`).
258
+ - **Call it when the player chooses to buy** ([§4](#4-leaving-takes-a-press-buying-does-not)).
259
+ No press is needed — an `await` before it is fine — but the player's press on your Buy button is
260
+ what they expect to open checkout, so make that button say plainly what it buys and for how much
261
+ (`priceDisplay`).
256
262
  - **It has no timeout.** A payment takes as long as it takes, so the call waits for as long as the
257
263
  Arcade is connected.
258
264
  - `ok`: the player paid. **Read balances and inventory through `gem.me`**; the answer carries no
@@ -266,10 +272,11 @@ arrives, so your own celebration is visible.
266
272
  ```
267
273
 
268
274
  - `declined` / `cancelled`: the player closed the checkout without paying. Return to your screen
269
- quietly and **do not ask again unprompted**: the Arcade backs off after the player closes one, and
270
- the next ask is `declined` / `busy` for a while, longer each time. An offer the player cannot buy
271
- again yet also answers `declined` / `cancelled`: the Arcade tells the player when it can be
272
- bought, opens nothing, and does not back off.
275
+ quietly and **do not ask again unprompted**. Nothing backs off: the player's next press opens the
276
+ checkout again, however many times they closed it. An offer the player cannot buy again yet also
277
+ answers `declined` / `cancelled`: the Arcade tells the player when it can be bought, and opens
278
+ nothing.
279
+ - `declined` / `busy`: another checkout or Arcade sheet was already open. Nothing was shown.
273
280
  - `unavailable` with `store_off`, `sign_in_required` or `not_found` (a SKU that is not in
274
281
  `offers()` right now), or with a payment provider's own failure code: nothing was bought. The
275
282
  first three show nothing.
@@ -304,13 +311,13 @@ pass the player already holds, how long it has left. The sheet's Buy opens the s
304
311
 
305
312
  Pick by who can say what the offer is:
306
313
 
307
- | | **You draw it: `offers()` + `purchase()`** | **The Arcade draws it: `showOffer()`** |
308
- | --------------- | --------------------------------------------------------- | ------------------------------------------------------------ |
309
- | Use it for | Gem packs: a name, an amount and a price say it all | VIP and other passes, and bundles |
310
- | Your button | says what it buys and shows `priceDisplay` | says what it opens, such as "Get VIP" |
311
- | The press opens | the checkout, straight away | the Arcade's sheet, then the checkout if the player presses Buy |
312
- | Perks and sales | not available to you | always current, in the player's language |
313
- | A holder sees | the same Buy button | what they already have |
314
+ | | **You draw it: `offers()` + `purchase()`** | **The Arcade draws it: `showOffer()`** |
315
+ | --------------- | --------------------------------------------------- | --------------------------------------------------------------- |
316
+ | Use it for | Gem packs: a name, an amount and a price say it all | VIP and other passes, and bundles |
317
+ | Your button | says what it buys and shows `priceDisplay` | says what it opens, such as "Get VIP" |
318
+ | The press opens | the checkout, straight away | the Arcade's sheet, then the checkout if the player presses Buy |
319
+ | Perks and sales | not available to you | always current, in the player's language |
320
+ | A holder sees | the same Buy button | what they already have |
314
321
 
315
322
  - **Never write an offer's perks into your game.** They change without a new build of your game, and
316
323
  yours would go stale. Your button only needs the offer's name.
@@ -328,10 +335,8 @@ Pick by who can say what the offer is:
328
335
 
329
336
  Every rule of `purchase` holds ([§9](#9-buying-from-the-arcade-store)), and the answers are the
330
337
  same, with one addition. `declined` / `cancelled` also covers the player leaving the sheet without
331
- pressing Buy, and you are not told which of the two they closed. That starts the same back-off as a
332
- closed checkout, shared with `purchase`: the next ask of either kind is `declined` / `busy` for a
333
- while. The press shows nothing during a back-off, so give the player something else to do on that
334
- screen.
338
+ pressing Buy, and you are not told which of the two they closed. Nothing backs off: the player's
339
+ next press on either button opens it again.
335
340
 
336
341
  ## 11. What the player holds, and a VIP door
337
342
 
@@ -445,14 +450,14 @@ player, and the route refuses it with `403`. The body is `{ pass, held, ends_at
445
450
  While something listens to `on('changed')`, the SDK reads again at each of these, and calls you only
446
451
  if the answer differs:
447
452
 
448
- | Moment | Why you cannot rely on something else |
449
- | --------------------------------------------------------- | -------------------------------------------------------------------------- |
450
- | `store.purchase` or `store.showOffer` answers `ok` | `onSettled` does not fire after a plain `ok` |
451
- | Either answers `unavailable` / `error` | The outcome is unknown, and the player may have paid |
452
- | `gem.store.onSettled` fires | `ok` / `pending` still reads as not held |
453
- | Your game resumes | A purchase in the Arcade's own store, from the Gem menu, sends no event at all |
454
- | The player signs in | A guest who signs in may hold VIP |
455
- | A held pass reaches `endsAt` | VIP does not renew, so the door has to lock by itself |
453
+ | Moment | Why you cannot rely on something else |
454
+ | -------------------------------------------------- | ------------------------------------------------------------------------------ |
455
+ | `store.purchase` or `store.showOffer` answers `ok` | `onSettled` does not fire after a plain `ok` |
456
+ | Either answers `unavailable` / `error` | The outcome is unknown, and the player may have paid |
457
+ | `gem.store.onSettled` fires | `ok` / `pending` still reads as not held |
458
+ | Your game resumes | A purchase in the Arcade's own store, from the Gem menu, sends no event at all |
459
+ | The player signs in | A guest who signs in may hold VIP |
460
+ | A held pass reaches `endsAt` | VIP does not renew, so the door has to lock by itself |
456
461
 
457
462
  A read that fails changes nothing, and is tried again later. A read that was already on its way when
458
463
  a purchase ended is never reused, so a stale answer cannot shut the door on the buyer. If you read
@@ -473,15 +478,15 @@ Until then, decide in a function, or leave a VIP door out of a dedicated-server
473
478
 
474
479
  ## 12. Outcomes and reasons
475
480
 
476
- | Call | `ok` | `declined` | `unavailable` |
477
- | ---------------- | ------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------- |
478
- | `appLinks` | listings to draw | | `unsupported`, `error` |
479
- | `openApp` | opened, or `handed_off` | `busy` | `already_in_app`, `unsupported`, `error` |
480
- | `openGame` | the next game is starting | `busy`, `same_game` | `not_found`, `unsupported`, `error` |
481
- | `similarGames` | cards, possibly none | | `unsupported`, `error` |
482
- | `store.offers` | offers, possibly none | | `sign_in_required`, `store_off`, `unsupported`, `error` |
483
- | `store.purchase` | paid, or `pending` | `cancelled`, `busy` | `sign_in_required`, `store_off`, `not_found`, the provider's reason, `unsupported`, `error` (outcome unknown) |
484
- | `store.showOffer` | paid, or `pending` | `cancelled`, `busy` | `sign_in_required`, `store_off`, `not_found`, the provider's reason, `unsupported`, `error` (outcome unknown) |
481
+ | Call | `ok` | `declined` | `unavailable` |
482
+ | ----------------- | ------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------- |
483
+ | `appLinks` | listings to draw | | `unsupported`, `error` |
484
+ | `openApp` | opened, or `handed_off` | `busy` | `already_in_app`, `unsupported`, `error` |
485
+ | `openGame` | the next game is starting | `busy`, `same_game` | `not_found`, `unsupported`, `error` |
486
+ | `similarGames` | cards, possibly none | | `unsupported`, `error` |
487
+ | `store.offers` | offers, possibly none | | `sign_in_required`, `store_off`, `unsupported`, `error` |
488
+ | `store.purchase` | paid, or `pending` | `cancelled`, `busy` | `sign_in_required`, `store_off`, `not_found`, the provider's reason, `unsupported`, `error` (outcome unknown) |
489
+ | `store.showOffer` | paid, or `pending` | `cancelled`, `busy` | `sign_in_required`, `store_off`, `not_found`, the provider's reason, `unsupported`, `error` (outcome unknown) |
485
490
 
486
491
  Every reason is a short code of lower-case letters and underscores. The Arcade turns a payment
487
492
  provider's own code into that shape before it reaches you.
package/docs/changelog.md CHANGED
@@ -16,6 +16,32 @@ 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
+
33
+ ## 20.0.0
34
+
35
+ Upgrading from 19.0.2. Impact: required.
36
+
37
+ ### Must change
38
+
39
+ - The parent conformance version moved from 3 to 4. If your game, test setup or tooling checks against the conformance version or runs the parent conformance checks, update it to expect version 4. Checks written for version 3 will now fail where they expect a purchase to be refused without a player press, or expect purchases to be refused after a player dismissed a checkout or sheet.
40
+
41
+ ### May stop
42
+
43
+ - Purchases no longer need a player press. A purchase asked from a timer or after an await opens its checkout, and the checkout is the confirmation. Games can drop code that holds a purchase request back until a tap or click. Games can also drop code that waits out a pause after a player dismisses a checkout or sheet, because the next request opens it again. Two things are unchanged: leaving the game still needs a press, and a purchase asked while another surface is open is still declined as busy.
44
+
19
45
  ## 19.0.2
20
46
 
21
47
  Upgrading from 19.0.1. Impact: none.
@@ -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()`, `quickJoin()` | Waits up to five seconds for the page to send a different list, and tries once more with it |
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 of them happens once.** A second refusal is the answer, and it reaches you as the error.
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
@@ -50,7 +50,7 @@ either way.
50
50
  ```jsonc
51
51
  // package.json
52
52
  "dependencies": {
53
- "@indiegems/gem-web-sdk": "19.0.2"
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, developer site and app
141
- are always allowed, and so is a dev host on your own machine (`gemctl serve`, the Gem Editor) while your
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.