@indiegems/gem-web-sdk 9.1.4 → 10.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.
@@ -746,16 +746,140 @@ for it would find it, and the room would still be briefly the wrong size — a v
746
746
  by a hidden one. The platform refuses the field outright, for this reason: a
747
747
  developer meets the rule locally rather than as a 400 in production.
748
748
 
749
+ ## 5c. Queueing for a match: tickets
750
+
751
+ `quickJoin()` puts a player in a room at once and lets the room fill around them. That is right for a
752
+ drop-in mode and wrong for one that must not start short — a duel, a team mode — because the first
753
+ player in sits alone in a lobby, and a match is sized by who is there when it starts.
754
+
755
+ `queueForMatch()` is the other way in. The player waits in a queue; when the platform has enough
756
+ players for one of the templates the ticket named, it makes the room, seats every one of them and
757
+ starts the match. Nobody hosts, and nobody calls `startMatch()`.
758
+
759
+ ```js
760
+ const ticket = await gem.rooms.queueForMatch({ templateSlugs: ['duel', 'brawl'] });
761
+ showSearching({ expiresAt: ticket.expiresAt, onCancel: () => ticket.cancel() });
762
+
763
+ const outcome = await ticket.outcome;
764
+ if (outcome.status === 'matched') enterMatch(outcome.room);
765
+ else backToMenu(outcome.status);
766
+ ```
767
+
768
+ **A template has to opt in.** A room template is queueable when its `matchmaking` block says
769
+ `enabled: true`; a ticket naming one that does not is refused whole, **400
770
+ `matchmaking_not_enabled`**. `fill_window_seconds` in that block is how long a queue holds out for
771
+ `max_players` before it accepts `min_players`. The block cannot be combined with `auto_start_host`.
772
+
773
+ **A ticket names every mode the player accepts**, one to eight templates, and is matched under
774
+ whichever fills first. That is one ticket, not several: a player can be on one ticket at a time, across
775
+ every game.
776
+
777
+ ### What the SDK does while the ticket waits
778
+
779
+ - **It keeps the ticket alive.** A ticket nobody heartbeats is expired by the platform, so that a
780
+ player who closed the tab does not hold a place in a lobby. The interval is the platform's setting
781
+ and arrives on the ticket; the SDK reads it from there and a game never sends a heartbeat.
782
+ - **The heartbeat is also how it learns the answer.** Each one returns the ticket as it stands, so a
783
+ match is noticed within one interval even when nothing crosses the bridge.
784
+ - **A relayed push makes it ask at once.** The match's own start is relayed like any room event (§3),
785
+ and one that arrives about a room the manager is not in, while a ticket waits, is treated as a cue:
786
+ the ticket is read immediately, and once more a second later if it still says `queued` — the
787
+ platform sends the push a moment before it marks the ticket. So on a page that relays, a match is
788
+ noticed in about the time the push takes, not at the next heartbeat. A tab returning to the
789
+ foreground is the same cue. Like every relay it is a hint: nothing is entered on its strength.
790
+ - **It refuses a second thing in flight.** While a ticket waits, `create`, `join`, `joinByCode`,
791
+ `quickJoin` and another `queueForMatch` are refused locally.
792
+ - **It cancels the ticket when the tab closes.** See below.
793
+ - **It raises no `seated` for the ticket's own room.** The owner is told by the outcome. Party
794
+ members, who hold no ticket, are told by `seated` as they are for any room their leader enters.
795
+
796
+ ### Queueing from inside a room
797
+
798
+ A player sitting in a hub or a social lobby can queue without leaving it. The ticket takes no seat
799
+ while it waits, and `gem.rooms.currentRoom` stays the room they are in.
800
+
801
+ When the ticket is matched, **the manager moves and the seat stays**. `currentRoom` becomes the
802
+ match's room and `joined` fires for it. The room the player queued from is not left: no leave is
803
+ sent, no `left` is raised, and the platform still counts them as a member there. When the match is
804
+ over, `gem.rooms.leave()` then `gem.rooms.join(hub.id)` puts them back — `join()` on a room a player
805
+ already holds a seat in answers with the room.
806
+
807
+ Keep the hub's id if you want that return trip: `currentRoom` is the match's room now, and the
808
+ manager does not hand the old one back.
809
+
810
+ **A tab closed before the player goes back leaves both rooms.** The SDK remembers the room a matched
811
+ ticket moved it out of, and on `pagehide` sends a keepalive leave for it as well as for the room it
812
+ is in. Otherwise the hub seat would outlive the page with nothing tracking it, and the player's next
813
+ session would start already inside a room. Once the player is back in the hub, it is the tracked
814
+ room again and the ordinary leave covers it.
815
+
816
+ ### When the tab closes
817
+
818
+ **The ticket is cancelled as the page goes away.** On `pagehide` the SDK sends the cancel as a
819
+ keepalive request, which the browser delivers after the document is gone, and settles the ticket
820
+ `cancelled`. Without it a closed tab's ticket would stay matchable until the platform noticed nobody
821
+ was keeping it alive, and a match formed in that window seats a player who is not there.
822
+
823
+ This also happens when the page is put in the back/forward cache. A frozen page keeps nothing alive,
824
+ so the player cannot come back to the same ticket; they come back to one that reads `cancelled` and
825
+ queue again.
826
+
827
+ The platform's own timeout is still there for a tab that crashed or lost its network, where no
828
+ request can be sent. It is the fallback, not the mechanism.
829
+
830
+ ### How a ticket ends
831
+
832
+ `ticket.outcome` settles once, and **resolves** for every ending the platform reports — these are
833
+ outcomes to branch on, not errors to catch:
834
+
835
+ | `status` | `reason` | What happened | Queue again? |
836
+ | --- | --- | --- | --- |
837
+ | `matched` | — | Seated, and the match is starting. `gem.rooms.currentRoom` is `outcome.room`, `joined` has fired, and the match events follow as for any room. | — |
838
+ | `expired` | `timed_out` | Nobody to play with before the ticket's expiry. | on a fresh press |
839
+ | `expired` | `heartbeat_lapsed` | The tab was throttled or offline too long to keep it alive. | on a fresh press |
840
+ | `cancelled` | `requested` | `ticket.cancel()`. | — |
841
+ | `cancelled` | `party_changed` | The party gained or lost a member while it waited. | once the party is settled |
842
+ | `failed` | `handoff_failed` | A match formed and could not be started, several times over. | later; this is the platform's fault |
843
+ | `lost` | — | The platform no longer has the ticket. Queues are held in memory and a restart loses them. | yes |
844
+ | `unavailable` | — | Matchmaking is turned off, or stopped answering for longer than a ticket survives. | not in a loop |
845
+
846
+ `lost` and `unavailable` are the SDK's own conclusions. A heartbeat answered "no such ticket" ends
847
+ the wait as `lost` at once. Three heartbeats in a row that got no usable answer end it too — as
848
+ `unavailable` when the platform said matchmaking is off, `lost` otherwise — and the SDK sends one
849
+ cancel on its way out, in case the requests arrived and only the answers were lost.
850
+
851
+ It **rejects** in one case: the ticket was matched and the room it was seated in could not be read. The
852
+ seat stands, so `myRooms()` shows the room and `join(id)` enters it.
853
+
854
+ **`cancel()` can lose the race.** A ticket the platform has already seated is never undone, so
855
+ `cancel()` resolves the ticket's real outcome, and that may be `matched`. Backing out from there is
856
+ `gem.rooms.leave()`.
857
+
858
+ ### The refusals when queueing
859
+
860
+ `queueForMatch()` itself rejects with `GemApiError` when no ticket was made. Branch on `code`:
861
+
862
+ | Code | Meaning |
863
+ | --- | --- |
864
+ | `already_queued` (409) | This player, or someone in their party, is already on a ticket — another tab, or another game. |
865
+ | `matchmaking_not_enabled` (400) | A named template has no enabled `matchmaking` block. A config fault, not a player one. |
866
+ | `matchmaking_unavailable` (503) | Matchmaking is turned off for the whole platform, or its store is not answering. |
867
+
868
+ ⚠️ **Nothing falls back to quick-join, and a game should not either.** Not on
869
+ `matchmaking_unavailable`, not on `expired`. Quick-join puts the player in a different kind of room —
870
+ alone, in a mode that was queued for precisely because it needs a lobby. Tell the player matchmaking is
871
+ unavailable and leave the button where it is.
872
+
749
873
  ## 6. Scope
750
874
 
751
875
  **In:** rooms, membership, host, ready state, the match lifecycle, and the dedicated-server connect
752
876
  flow reshaped around them. P2P _allocation_ falls out of the same shape — `startMatch` is one route
753
877
  and a P2P match simply reports `ready` at once.
754
878
 
755
- **Out:** matchmaking beyond the engine's find-or-create. `quickJoin()` is exactly `quick_join`: join
756
- an open public room matching the template, in a cluster this player can play well in, or create one.
757
- There is no ranked
758
- system, no skill rating and no queue, and inventing one is not a small addition to this.
879
+ **Out:** matching by anything but arrival order. `quickJoin()` is exactly `quick_join`: join an open
880
+ public room matching the template, in a cluster this player can play well in, or create one.
881
+ `queueForMatch()` (§5c) waits for a full lobby, oldest ticket first. There is no ranked system and no
882
+ skill rating, and inventing one is not a small addition to this.
759
883
 
760
884
  **Elsewhere:** P2P _connection_ and parties. Both are
761
885
  [`p2p-and-parties.md`](p2p-and-parties.md). Three things surface here:
@@ -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/v9.1.4/gem-web-sdk.tgz
84
- curl -fsSL https://downloads.gemarcade.com/sdk/v9.1.4/gem-web-sdk.tgz.sri
83
+ curl -fsSL -O https://downloads.gemarcade.com/sdk/v10.0.0/gem-web-sdk.tgz
84
+ curl -fsSL https://downloads.gemarcade.com/sdk/v10.0.0/gem-web-sdk.tgz.sri
85
85
  openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
86
86
  ```
87
87
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indiegems/gem-web-sdk",
3
- "version": "9.1.4",
3
+ "version": "10.0.0",
4
4
  "description": "postMessage bridge contract between gem-web and browser-playable games",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",