@indiegems/gem-web-sdk 20.0.0 → 21.0.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/brand/ticket-hero-128.png +0 -0
  3. package/brand/ticket-hero-256.png +0 -0
  4. package/brand/ticket-hero-384.png +0 -0
  5. package/brand/ticket-hero-512.png +0 -0
  6. package/brand/ticket-hero-768.png +0 -0
  7. package/brand/ticket-icon-20-1.5x.png +0 -0
  8. package/brand/ticket-icon-20-1x.png +0 -0
  9. package/brand/ticket-icon-20-2x.png +0 -0
  10. package/brand/ticket-icon-20-3x.png +0 -0
  11. package/brand/ticket-icon-32-1.5x.png +0 -0
  12. package/brand/ticket-icon-32-1x.png +0 -0
  13. package/brand/ticket-icon-32-2x.png +0 -0
  14. package/brand/ticket-icon-32-3x.png +0 -0
  15. package/brand/ticket-icon-ingame-20-1.5x.png +0 -0
  16. package/brand/ticket-icon-ingame-20-1x.png +0 -0
  17. package/brand/ticket-icon-ingame-20-2x.png +0 -0
  18. package/brand/ticket-icon-ingame-20-3x.png +0 -0
  19. package/brand/ticket-icon-ingame-32-1.5x.png +0 -0
  20. package/brand/ticket-icon-ingame-32-1x.png +0 -0
  21. package/brand/ticket-icon-ingame-32-2x.png +0 -0
  22. package/brand/ticket-icon-ingame-32-3x.png +0 -0
  23. package/brand/ticket-pile-1-128.png +0 -0
  24. package/brand/ticket-pile-1-256.png +0 -0
  25. package/brand/ticket-pile-1-384.png +0 -0
  26. package/brand/ticket-pile-1-512.png +0 -0
  27. package/brand/ticket-pile-1-768.png +0 -0
  28. package/brand/ticket-pile-2-128.png +0 -0
  29. package/brand/ticket-pile-2-256.png +0 -0
  30. package/brand/ticket-pile-2-384.png +0 -0
  31. package/brand/ticket-pile-2-512.png +0 -0
  32. package/brand/ticket-pile-2-768.png +0 -0
  33. package/dist/function.d.ts +1 -1
  34. package/dist/game.d.ts +52 -4
  35. package/dist/gem-web-sdk.bridge.js +2 -2
  36. package/dist/gem-web-sdk.conformance.js +3 -3
  37. package/dist/gem-web-sdk.esm.js +3 -3
  38. package/dist/gem-web-sdk.function.js +3 -3
  39. package/dist/gem-web-sdk.game.js +3 -3
  40. package/dist/gem-web-sdk.min.js +3 -3
  41. package/dist/gem-web-sdk.sdk.js +3 -3
  42. package/dist/gem-web-sdk.server.js +2 -2
  43. package/dist/index.d.ts +57 -6
  44. package/dist/sdk.d.ts +52 -4
  45. package/docs/analytics.md +11 -0
  46. package/docs/api/api-reference-function.md +7 -2
  47. package/docs/api/api-reference-game.md +15 -9
  48. package/docs/api/api-reference-server.md +1 -1
  49. package/docs/api/reference.json +8 -8
  50. package/docs/brand-art.md +65 -16
  51. package/docs/changelog.md +31 -0
  52. package/docs/dedicated-server-flow.md +30 -2
  53. package/docs/getting-started.md +3 -3
  54. package/docs/manifest.json +18 -18
  55. package/docs/match-session.md +9 -0
  56. package/docs/p2p-and-parties.md +79 -6
  57. package/docs/server-bundles.md +2 -2
  58. package/docs/what-a-game-must-do.md +8 -5
  59. package/package.json +1 -1
  60. package/schema/bridge.v1.json +67 -2
  61. package/schema/capabilities.public.json +6 -0
@@ -13,8 +13,8 @@
13
13
  },
14
14
  {
15
15
  "file": "analytics.md",
16
- "sha256": "42f3d0d3360e1a6066c21015bd4925475b4336a53611bf04acb773a7c6084e62",
17
- "updated": "2026-10-30T16:54:41Z"
16
+ "sha256": "0883e3c53f500f4d0554d7d62ef938bc53676800b11a285e0da508c6fcc4076d",
17
+ "updated": "2026-11-02T22:24:57Z"
18
18
  },
19
19
  {
20
20
  "file": "arcade-and-store.md",
@@ -38,8 +38,8 @@
38
38
  },
39
39
  {
40
40
  "file": "brand-art.md",
41
- "sha256": "b7f8dda1afdb8d56cadee752e6b09fc62238372b68ebc8a746518e528f52e167",
42
- "updated": "2026-10-29T21:40:17Z"
41
+ "sha256": "7d4d5c7226710742efdb958e10387d5e5a3de0622a686734c885720e16a5b128",
42
+ "updated": "2026-11-02T21:18:05Z"
43
43
  },
44
44
  {
45
45
  "file": "branding.md",
@@ -48,8 +48,8 @@
48
48
  },
49
49
  {
50
50
  "file": "changelog.md",
51
- "sha256": "37f3bde69baa7c0280e7d8a58800c1509b3b61c76a684428832bf094f298478b",
52
- "updated": "2026-11-02T19:11:02Z"
51
+ "sha256": "ed2476deed910a61badb23023267b5a8292467d3812c211adf3baf23c42ae914",
52
+ "updated": "2026-11-03T21:36:35Z"
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": "6da1bfc820ab2cf79180f69f9fec4d9f377ddde690f53923e749a88ec6307e22",
62
- "updated": "2026-10-02T19:04:21Z"
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": "896f7d29d2017e5c42fec20386bce31eacf3f5143b7134c7607574b452c039bf",
82
- "updated": "2026-11-02T19:11:02Z"
81
+ "sha256": "dd7328fd2462efd7373904f1c14085fd75b046d76670113cd0762825d0c0f107",
82
+ "updated": "2026-11-03T21:36:35Z"
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": "cfc2f76e310f4a851ac0b9185687d284cced6533fe12fb359ce0cb2845c5e5ef",
97
- "updated": "2026-10-23T01:58:45Z"
96
+ "sha256": "121eb2bd92da9daf3922097699e0ab450da6a087eec8de552fe573094cf7e2c4",
97
+ "updated": "2026-11-02T16:12:53Z"
98
98
  },
99
99
  {
100
100
  "file": "p2p-and-parties.md",
101
- "sha256": "4cfe2a47def51eb617f154eaac39faf176d8aa7ca2c36907084b6349048ab1b9",
102
- "updated": "2026-10-17T22:11:15Z"
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": "655a5835bf9d1c13997784de57aaf54566d72641bfe220da8a37903d5fad044c",
142
- "updated": "2026-11-02T19:11:02Z"
141
+ "sha256": "736e7b4ceb810944331274f940cb47e7d3d4a57f815340a9ade7ffc5f01cd0fa",
142
+ "updated": "2026-11-03T21:36:35Z"
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": "17df2ebdafe56ac5eafdf4eba783c97025ddd9e0ee88b7b43d25718908496b2a",
167
- "updated": "2026-11-02T14:57:41Z"
166
+ "sha256": "059f3c8a31c0f6098d2c8e4836b79e866d0c5486443dc89039e160648946bd68",
167
+ "updated": "2026-11-02T23:27:22Z"
168
168
  }
169
169
  ]
170
170
  }
@@ -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
@@ -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 that sees ICE fail reports the host unreachable **once per epoch**.
392
- That is a vote, not a verdict: gem-api migrates on a quorum of independent
393
- reporters and never on one, so it cannot unseat a host who is fine for everyone
394
- else.
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 `host_unreachable` when the host's
402
- link fails, `matchReconnected` with `attempt: 0` when the migration seated a
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
@@ -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.0.0/gem-web-sdk.tgz
84
- curl -fsSL https://downloads.gemarcade.com/sdk/v20.0.0/gem-web-sdk.tgz.sri
83
+ curl -fsSL -O https://downloads.gemarcade.com/sdk/v21.0.0/gem-web-sdk.tgz
84
+ curl -fsSL https://downloads.gemarcade.com/sdk/v21.0.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, developer site and app** — the SDK carries their origins as
57
- `ARCADE_EMBEDDERS`, on every platform the app runs on. The developer site,
58
- `https://developer.gemarcade.com`, is where you play your own dev and test builds; a build accepts
59
- it once it is built with an SDK that lists it;
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. You write nothing for it. It stays out of your way:
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indiegems/gem-web-sdk",
3
- "version": "20.0.0",
3
+ "version": "21.0.0",
4
4
  "description": "postMessage bridge contract between gem-web and browser-playable games",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -1790,8 +1790,11 @@
1790
1790
  "deliberately: a frame that took its JWKS endpoint from a relayed message",
1791
1791
  "would let the parent choose the keys it verifies peers against.",
1792
1792
  "",
1793
- "The nine events are the whole vocabulary; an unrecognised p2p_* type is",
1794
- "a drop at the parent, not a passthrough.",
1793
+ "The nine events are the whole vocabulary of this message; an",
1794
+ "unrecognised p2p_* type is a drop at the parent, not a passthrough. The",
1795
+ "relay drain notice is the one further push the parent relays, and it",
1796
+ "crosses as its own type, gem.relayDraining: this enum is closed, so a",
1797
+ "tenth value would be refused by every frame already shipped.",
1795
1798
  "",
1796
1799
  "PARENT TO FRAME ONLY. gem.signal is the other direction."
1797
1800
  ],
@@ -1824,6 +1827,68 @@
1824
1827
  "rateLimit": { "perMinute": 1800 }
1825
1828
  },
1826
1829
 
1830
+ {
1831
+ "type": "gem.relayDraining",
1832
+ "direction": "parentToFrame",
1833
+ "transport": "port",
1834
+ "$comment": [
1835
+ "A TURN relay this player's P2P match may be using is going away, relayed",
1836
+ "from gem-api's `p2p_relay_draining`. Gated by the `p2pRelayDrain`",
1837
+ "capability; the mapping lives in schema/capabilities.json and nowhere",
1838
+ "else.",
1839
+ "",
1840
+ "A HINT, AND ONE THE FRAME COULD SURVIVE WITHOUT. It tells the frame to",
1841
+ "re-lease and, where its selected candidate pair runs through one of",
1842
+ "these relays, to restart ICE on the connection it already has, so the",
1843
+ "match moves before the relay stops rather than after. The control is",
1844
+ "the relay's own: a draining relay refuses new allocations whatever",
1845
+ "this message says, and a connection on it fails over by the ordinary",
1846
+ "path when it stops. A parent that rewrites this can cost a frame a",
1847
+ "renegotiation; it cannot move a connection anywhere.",
1848
+ "",
1849
+ "ITS OWN TYPE, NOT A TENTH VALUE IN gem.signalEvent'S ENUM: the",
1850
+ "generated validator rejects an unknown enum value, and every frame",
1851
+ "already shipped would answer `invalid_message`. A parent sends this",
1852
+ "only to a frame that advertised the capability and drops it otherwise.",
1853
+ "",
1854
+ "SCOPED BY MATCH, NOT BY GAME. gem-api stamps the push with no game id,",
1855
+ "as it does `p2p_error`, so the parent relays it only for a match this",
1856
+ "player's socket or this frame has already named.",
1857
+ "",
1858
+ "PARENT TO FRAME ONLY. A frame-authored drain notice would let a game",
1859
+ "tell the parent a relay is going away."
1860
+ ],
1861
+ "fields": {
1862
+ "matchId": {
1863
+ "kind": "string",
1864
+ "maxLength": 64,
1865
+ "$comment": "The match whose relays are draining. The frame acts only for the live session on this match."
1866
+ },
1867
+ "clusterTargetId": {
1868
+ "kind": "string",
1869
+ "maxLength": 64,
1870
+ "$comment": "The cluster the draining relays belong to. It decides nothing on the frame: the re-lease asks the cluster the session already leases from."
1871
+ },
1872
+ "relayIps": {
1873
+ "kind": "array",
1874
+ "maxItems": 16,
1875
+ "items": { "kind": "string", "maxLength": 64 },
1876
+ "$comment": "The draining relays' public addresses — the hosts in the lease's `turn:` URLs, and what a selected candidate pair names. A lease names at most three relays."
1877
+ },
1878
+ "drainingSince": {
1879
+ "kind": "string",
1880
+ "maxLength": 40,
1881
+ "$comment": "RFC 3339. When the relays began draining."
1882
+ },
1883
+ "deadline": {
1884
+ "kind": "string",
1885
+ "maxLength": 40,
1886
+ "$comment": "RFC 3339. When the relays stop relaying. A link still on one by then is dropped and re-dialled; one this side cannot parse is read as the drain ceiling, thirty minutes."
1887
+ }
1888
+ },
1889
+ "rateLimit": { "perMinute": 60 }
1890
+ },
1891
+
1827
1892
  {
1828
1893
  "type": "gem.dedicatedEvent",
1829
1894
  "direction": "parentToFrame",
@@ -308,6 +308,12 @@
308
308
  "gates": [
309
309
  "gem.menuStyle"
310
310
  ]
311
+ },
312
+ "p2pRelayDrain": {
313
+ "implemented": true,
314
+ "gates": [
315
+ "gem.relayDraining"
316
+ ]
311
317
  }
312
318
  }
313
319
  }