@indiegems/gem-web-sdk 11.1.2 → 12.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.
- package/CHANGELOG.md +14 -0
- package/README.md +3 -1
- package/dist/game.d.ts +49 -0
- package/dist/gem-web-sdk.bridge.js +1 -1
- package/dist/gem-web-sdk.esm.js +2 -2
- package/dist/gem-web-sdk.function.js +2 -2
- package/dist/gem-web-sdk.game.js +2 -2
- package/dist/gem-web-sdk.min.js +2 -2
- package/dist/gem-web-sdk.sdk.js +2 -2
- package/dist/gem-web-sdk.server.js +2 -2
- package/dist/index.d.ts +66 -4
- package/dist/sdk.d.ts +49 -0
- package/docs/api/api-reference-function.md +7916 -0
- package/docs/api/api-reference-game.md +191 -3
- package/docs/api/api-reference-server.md +5 -3
- package/docs/api/reference.json +9491 -3
- package/docs/changelog.md +14 -0
- package/docs/getting-started.md +5 -2
- package/docs/manifest.json +12 -12
- package/docs/rooms-and-matches.md +64 -1
- package/docs/server-bundles.md +2 -2
- package/docs/serverless-functions-js.md +118 -44
- package/docs/serverless-functions.md +26 -11
- package/package.json +1 -1
- package/schema/bridge.v1.json +84 -0
- package/schema/capabilities.public.json +6 -0
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
|
+
## 12.0.0
|
|
20
|
+
|
|
21
|
+
Upgrading from 11.1.2. Impact: required.
|
|
22
|
+
|
|
23
|
+
### Rebuild
|
|
24
|
+
|
|
25
|
+
- The list of capability names gained ticketEvents. Recompile your game. If your code switches exhaustively over capability names, add a case for the new one.
|
|
26
|
+
- The list of messages the parent sends to the frame, and the union of parent message types, each gained a ticket event message. Recompile your game. If your code switches exhaustively over these message names or types, handle or ignore the new one.
|
|
27
|
+
|
|
28
|
+
### New capability
|
|
29
|
+
|
|
30
|
+
- A party member's game can now show the matchmaking ticket its party is queued on. Read the ticket through the rooms manager, or listen for the new partyQueued room event. The view is read-only: it has a status, a party size, two instants and an outcome, and it cannot be cancelled. A matched ticket does not carry the room. The room is reported separately once the SDK has confirmed the player is seated in it. The view ends on its own as lost once the ticket's lifetime has passed with no ending heard. For the player who queued, the ticket still comes from the platform's own answer.
|
|
31
|
+
- Games that build their own frame can now pass an onTicketEvent callback to receive ticket state changes pushed by the parent. It is delivered only when the parent relays ticket events and advertises the new ticketEvents capability. Under a parent that does not, nothing changes and the callback is never called. Ticket events name no player and do not carry the templates a ticket named.
|
|
32
|
+
|
|
19
33
|
## 11.1.2
|
|
20
34
|
|
|
21
35
|
Upgrading from 11.1.1. Impact: none.
|
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": "
|
|
53
|
+
"@indiegems/gem-web-sdk": "12.0.0"
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -66,7 +66,10 @@ is involved, and nothing is built when you install.
|
|
|
66
66
|
**`/sdk` is the import, from every seat.** It carries the whole surface, so client code, a
|
|
67
67
|
dedicated server bundle and a listen host all import the same path and the same names.
|
|
68
68
|
`.`, `/game` and `/server` stay published as narrow doors over the same modules — a game
|
|
69
|
-
wants none of them unless it knows why.
|
|
69
|
+
wants none of them unless it knows why. `/function` is the fourth door, for code that runs as
|
|
70
|
+
a serverless function ([serverless-functions-js.md](serverless-functions-js.md)). The function
|
|
71
|
+
starter's build resolves `/sdk` and `/game` to it, so verification code shared with your game
|
|
72
|
+
keeps its import.
|
|
70
73
|
|
|
71
74
|
**`dependencies`, not `devDependencies`.** The SDK is part of what your game ships — your
|
|
72
75
|
bundler compiles it into the browser build and into the server bundle — so a `--production`
|
package/docs/manifest.json
CHANGED
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"file": "changelog.md",
|
|
36
|
-
"sha256": "
|
|
37
|
-
"updated": "2026-10-
|
|
36
|
+
"sha256": "bf2786be31121bb229d10d98f701165aefb2e7166271299450c926401661dfd2",
|
|
37
|
+
"updated": "2026-10-12T22:12:20Z"
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"file": "cross-promotion.md",
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
"file": "getting-started.md",
|
|
71
|
-
"sha256": "
|
|
72
|
-
"updated": "2026-10-
|
|
71
|
+
"sha256": "19bf05cd8f7afe5e9f406af6aef090a73bb3c2fb713f06185ead36fc2bb104dc",
|
|
72
|
+
"updated": "2026-10-12T22:12:20Z"
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
75
|
"file": "inventory-and-catalogs.md",
|
|
@@ -113,8 +113,8 @@
|
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"file": "rooms-and-matches.md",
|
|
116
|
-
"sha256": "
|
|
117
|
-
"updated": "2026-10-
|
|
116
|
+
"sha256": "29cf42a4334d3d9702e6a7f6e04c52fb2b414af3340043c2f98b2bfafa7dc64d",
|
|
117
|
+
"updated": "2026-10-12T19:59:08Z"
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
120
|
"file": "saving-player-progress.md",
|
|
@@ -123,18 +123,18 @@
|
|
|
123
123
|
},
|
|
124
124
|
{
|
|
125
125
|
"file": "server-bundles.md",
|
|
126
|
-
"sha256": "
|
|
127
|
-
"updated": "2026-10-
|
|
126
|
+
"sha256": "52054ceeb56b7130bfaf3461c2d81a9bd3b3dcd0f95097753ebfffc08f7a1cbb",
|
|
127
|
+
"updated": "2026-10-12T22:12:20Z"
|
|
128
128
|
},
|
|
129
129
|
{
|
|
130
130
|
"file": "serverless-functions-js.md",
|
|
131
|
-
"sha256": "
|
|
132
|
-
"updated": "2026-10-
|
|
131
|
+
"sha256": "3904704418f9c6c246c8e1fa83f96901f6e493a3f4499b16fd6845620515e57d",
|
|
132
|
+
"updated": "2026-10-12T17:39:05Z"
|
|
133
133
|
},
|
|
134
134
|
{
|
|
135
135
|
"file": "serverless-functions.md",
|
|
136
|
-
"sha256": "
|
|
137
|
-
"updated": "2026-10-
|
|
136
|
+
"sha256": "84881bcc40c6203c731ebf9092be575158d0ad3b3a65120321b695ae6558f63f",
|
|
137
|
+
"updated": "2026-10-12T17:39:05Z"
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
"file": "trading-and-gifting.md",
|
|
@@ -140,6 +140,13 @@ frame that advertises `autoStartFailure`. It is not a sixth `gem.roomEvent` beca
|
|
|
140
140
|
refuses an enum value it does not know, and reports that to the game as a protocol error; an unknown
|
|
141
141
|
message type it ignores. The game sees it as `matchAutoStartFailed` (§4.4).
|
|
142
142
|
|
|
143
|
+
**The matchmaking-ticket pushes cross the same way**, as `gem.ticketEvent`, to a frame that
|
|
144
|
+
advertises `ticketEvents`. The platform tells every player a ticket seats when it is created,
|
|
145
|
+
matched, or ended without a match, and the parent relays each one for the frame's own game: the
|
|
146
|
+
ticket's id and new status, plus the party size and the two instants on creation, the reason on an
|
|
147
|
+
ending without a match, and the room to look in on a match. The templates the ticket named do not
|
|
148
|
+
cross, and no player is named on either side of the bridge. What a game does with it is §5c.
|
|
149
|
+
|
|
143
150
|
There is exactly one filtering point and this is it. The engine keeps a full match event in native
|
|
144
151
|
code and hands its scripting layer an allowlisted subset, because native code can hold a secret
|
|
145
152
|
from a sandboxed script. **The frame-side SDK cannot** — it shares a JavaScript realm with the game,
|
|
@@ -794,11 +801,15 @@ every game.
|
|
|
794
801
|
platform sends the push a moment before it marks the ticket. So on a page that relays, a match is
|
|
795
802
|
noticed in about the time the push takes, not at the next heartbeat. A tab returning to the
|
|
796
803
|
foreground is the same cue. Like every relay it is a hint: nothing is entered on its strength.
|
|
804
|
+
**A relayed ticket push is the same cue**, so an ending without a match is noticed as quickly as a
|
|
805
|
+
match is. The ticket's outcome still comes from the platform's answer, never from the push.
|
|
797
806
|
- **It refuses a second thing in flight.** While a ticket waits, `create`, `join`, `joinByCode`,
|
|
798
807
|
`quickJoin` and another `queueForMatch` are refused locally.
|
|
799
808
|
- **It cancels the ticket when the tab closes.** See below.
|
|
800
809
|
- **It raises no `seated` for the ticket's own room.** The owner is told by the outcome. Party
|
|
801
|
-
members, who hold no ticket, are told by `seated` as they are for any room their leader enters
|
|
810
|
+
members, who hold no ticket, are told by `seated` as they are for any room their leader enters —
|
|
811
|
+
and, on a page that relays ticket events, by `partyQueued` while the party waits. See
|
|
812
|
+
[A party member's view](#a-party-members-view-of-the-ticket).
|
|
802
813
|
|
|
803
814
|
### Queueing from inside a room
|
|
804
815
|
|
|
@@ -862,6 +873,58 @@ seat stands, so `myRooms()` shows the room and `join(id)` enters it.
|
|
|
862
873
|
`cancel()` resolves the ticket's real outcome, and that may be `matched`. Backing out from there is
|
|
863
874
|
`gem.rooms.leave()`.
|
|
864
875
|
|
|
876
|
+
### A party member's view of the ticket
|
|
877
|
+
|
|
878
|
+
A party queues as **one ticket, and it is the leader's**. Only the leader calls `queueForMatch()`,
|
|
879
|
+
and only a ticket's owner can read it, keep it alive or cancel it. So a member's game holds no
|
|
880
|
+
ticket — and without help it knows nothing about the queue its player is standing in until the match
|
|
881
|
+
pulls them in.
|
|
882
|
+
|
|
883
|
+
On an arcade page that relays ticket events, the member's game is told. When the leader queues,
|
|
884
|
+
`gem.rooms.partyTicket` is set and `partyQueued` is raised with it:
|
|
885
|
+
|
|
886
|
+
```js
|
|
887
|
+
gem.rooms.on('partyQueued', async (ticket) => {
|
|
888
|
+
showSearching({ expiresAt: ticket.expiresAt, canCancel: false });
|
|
889
|
+
const outcome = await ticket.outcome;
|
|
890
|
+
if (outcome.status !== 'matched') backToMenu(outcome.status);
|
|
891
|
+
// `matched`: the room arrives through your `seated` handler.
|
|
892
|
+
});
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
- **It is a view. There is no `cancel()`.** The ticket is the leader's to cancel. A member who wants
|
|
896
|
+
out leaves the party, which ends the ticket for everyone with reason `party_changed`. Draw the
|
|
897
|
+
searching screen without a leave-queue button.
|
|
898
|
+
- **It costs no request.** Nothing is heartbeated and nothing is polled for it; everything the view
|
|
899
|
+
says came from a relayed push.
|
|
900
|
+
- **`matched` carries no room.** The room is reported the way a party's seat always is, the `seated`
|
|
901
|
+
event, and that handler is where a game joins it. The SDK looks for the seat as soon as it hears
|
|
902
|
+
`matched`, and reports it only after reading the room over the game's own token and finding this
|
|
903
|
+
player in it.
|
|
904
|
+
- **It ends by itself.** The pushes are sent once. A member's game that missed the ending would
|
|
905
|
+
otherwise show a searching screen for ever, so the view ends as `lost` a few seconds after the
|
|
906
|
+
ticket's lifetime has run out with nothing more heard. A seat found by the SDK's own seat check
|
|
907
|
+
ends it as `matched`, whether or not that push arrived.
|
|
908
|
+
- **Never set beside `currentTicket`.** The player who queued holds the ticket itself and gets no
|
|
909
|
+
`partyQueued`.
|
|
910
|
+
|
|
911
|
+
| `status` | `reason` | What happened |
|
|
912
|
+
| --- | --- | --- |
|
|
913
|
+
| `matched` | — | The party is seated and the match is starting. `seated` has the room. |
|
|
914
|
+
| `expired` | `timed_out` | Nobody to play with before the ticket's expiry. |
|
|
915
|
+
| `expired` | `heartbeat_lapsed` | The leader's game stopped keeping the ticket alive: a closed, throttled or offline tab. |
|
|
916
|
+
| `cancelled` | `requested` | The leader cancelled. |
|
|
917
|
+
| `cancelled` | `party_changed` | The party gained or lost a member while it waited. |
|
|
918
|
+
| `failed` | `handoff_failed` | A match formed and could not be started, several times over. |
|
|
919
|
+
| `lost` | — | The SDK's own conclusion: the ticket's lifetime ran out and no ending was heard. |
|
|
920
|
+
|
|
921
|
+
⚠️ **Build it as an extra, never as a step.** The view exists only where the page relays ticket
|
|
922
|
+
events, and only for a game that was already running when the leader queued: a push sent before the
|
|
923
|
+
game was listening is not sent again. In every other case `partyTicket` stays `null`, no
|
|
924
|
+
`partyQueued` is raised, and the member is carried into the match by `seated` with no lead-up. That
|
|
925
|
+
path has to keep working, so the `seated` handler is what enters the match and the searching screen
|
|
926
|
+
is only something shown on the way.
|
|
927
|
+
|
|
865
928
|
### The refusals when queueing
|
|
866
929
|
|
|
867
930
|
`queueForMatch()` itself rejects with `GemApiError` when no ticket was made. Branch on `code`:
|
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/
|
|
84
|
-
curl -fsSL https://downloads.gemarcade.com/sdk/
|
|
83
|
+
curl -fsSL -O https://downloads.gemarcade.com/sdk/v12.0.0/gem-web-sdk.tgz
|
|
84
|
+
curl -fsSL https://downloads.gemarcade.com/sdk/v12.0.0/gem-web-sdk.tgz.sri
|
|
85
85
|
openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
|
|
86
86
|
```
|
|
87
87
|
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
# Serverless functions in JavaScript
|
|
2
2
|
|
|
3
|
-
**The platform does not run JavaScript functions yet.** This page describes how one is written
|
|
4
|
-
and how it behaves; today a function you publish is a WebAssembly component, as
|
|
5
|
-
[serverless-functions.md](serverless-functions.md) describes.
|
|
6
|
-
|
|
7
3
|
A serverless function can be written in JavaScript or TypeScript instead of Rust: the language your
|
|
8
4
|
game is already written in, so the verification code in your browser game moves into a function
|
|
9
5
|
with no change beyond bundling. You upload one bundled ES module, and the platform runs it on a
|
|
@@ -51,31 +47,46 @@ are read and written:
|
|
|
51
47
|
| `handle(input)` | `Uint8Array`: the request body, byte for byte | `Uint8Array`, `ArrayBuffer` or `string` (sent as UTF-8); anything else is an error |
|
|
52
48
|
| `handleJson(input)` | the body parsed as JSON; an empty body is `undefined` | any JSON-serializable value, sent as its JSON text; `undefined` is sent as `null`; a `BigInt` is an error |
|
|
53
49
|
|
|
54
|
-
Either may be `async`; the platform waits for the promise
|
|
55
|
-
when you upload. A caller cannot tell which kind of
|
|
56
|
-
limits and the refusals in serverless-functions.md
|
|
50
|
+
Either may be `async`; the platform waits for the promise, and a promise that never settles fails
|
|
51
|
+
the call. Exporting neither, or both, is refused when you upload. A caller cannot tell which kind of
|
|
52
|
+
function answered: `gem.functions.invoke`, the limits and the refusals in serverless-functions.md
|
|
53
|
+
are identical for both.
|
|
54
|
+
|
|
55
|
+
**Each call gets the same limits as a component:** 5 seconds of wall clock, 1 second of CPU,
|
|
56
|
+
256 MiB of memory, 32 platform calls, 256 KiB of input and 1 MiB of output, and the module itself
|
|
57
|
+
may be at most 32 MiB. The full list is
|
|
58
|
+
[the limits on one call](serverless-functions.md#the-limits-on-one-call).
|
|
57
59
|
|
|
58
60
|
**A thrown exception is `Err`.** The call fails with `function_error`, and `err.functionMessage` is
|
|
59
61
|
the exception's `name: message` (a thrown value that is not an `Error` is its `String()`). The
|
|
60
|
-
message and the stack
|
|
61
|
-
`handleJson` whose input is not JSON, or whose return value JSON cannot carry, fails the
|
|
62
|
+
message and the stack are also logged for the invocation at `error`; the stack never reaches the
|
|
63
|
+
caller. A `handleJson` whose input is not JSON, or whose return value JSON cannot carry, fails the
|
|
64
|
+
same way.
|
|
62
65
|
|
|
63
66
|
**`console.*` reaches the invocation's log** at the matching level (`debug`, `info`, `warn`,
|
|
64
67
|
`error`); `console.log` and every other method log at `info`. As with a component's `log`,
|
|
65
68
|
`gemctl function logs` reads it back for a call on `dev`, and a call on `test` or `release` keeps
|
|
66
|
-
no line.
|
|
69
|
+
no line. Each line is cut at 1 KiB, and one call keeps at most **100 lines or 64 KiB** of them,
|
|
70
|
+
whichever comes first. Every `console` call past that goes unrecorded, and the call's closing log
|
|
71
|
+
line counts them in `dropped_log_lines` (serverless-functions.md section 7).
|
|
67
72
|
|
|
68
73
|
## 2. Writing one
|
|
69
74
|
|
|
70
75
|
```bash
|
|
71
76
|
gemctl function init --lang js functions/score-check
|
|
72
|
-
cd functions/score-check && npm install && npm run build
|
|
73
|
-
gemctl function upload dist/function.js
|
|
77
|
+
(cd functions/score-check && npm install && npm run build)
|
|
78
|
+
gemctl function upload --file functions/score-check/dist/function.js --version 1.0.0
|
|
79
|
+
gemctl function promote 1.0.0 --channel dev
|
|
74
80
|
```
|
|
75
81
|
|
|
76
82
|
**`init --lang js`** writes a `package.json` whose `build` script type-checks and then bundles to
|
|
77
|
-
one ES module
|
|
78
|
-
`npm run build` is yours, the way `cargo build` is for a component.
|
|
83
|
+
one ES module in `dist`, a TypeScript starter handler and a `tsconfig.json`. gemctl runs neither
|
|
84
|
+
step: `npm run build` is yours, the way `cargo build` is for a component. The starter lists the SDK
|
|
85
|
+
in `devDependencies`, and its build bundles the SDK into the one module you upload. Nothing is
|
|
86
|
+
provided to a function you upload now, so that module is the whole of your function. That is why
|
|
87
|
+
the SDK is a dev dependency here while [getting-started.md](getting-started.md) puts it in
|
|
88
|
+
`dependencies` for a game: what a game ships is built from its install, and what a function ships
|
|
89
|
+
is the bundle.
|
|
79
90
|
|
|
80
91
|
A handler looks like this:
|
|
81
92
|
|
|
@@ -96,23 +107,38 @@ The package is the one your game already depends on, installed as
|
|
|
96
107
|
serverless-functions.md section 1, and it applies unchanged: a save or a setting the player's own
|
|
97
108
|
browser can write is the player's claim, not evidence.
|
|
98
109
|
|
|
99
|
-
**Imports.** The module
|
|
100
|
-
|
|
101
|
-
`@indiegems/gem-web-sdk/
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
**Imports.** The module you upload imports nothing. Everything it uses, the SDK included, is
|
|
111
|
+
bundled into it, which is what the starter's build does. A file shared with your game may keep its
|
|
112
|
+
`@indiegems/gem-web-sdk/sdk` or `@indiegems/gem-web-sdk/game` import, because the starter's build
|
|
113
|
+
resolves both to `@indiegems/gem-web-sdk/function` when it bundles your function. A name the
|
|
114
|
+
function entry does not export, such as `defineServer`, then fails the build rather than a call.
|
|
115
|
+
For a function still on the engine's built-in copy (below) the same name fails the call with
|
|
116
|
+
`function_error`. An `import()` at runtime is refused at upload. Your module is evaluated afresh
|
|
117
|
+
for every call, so its top level, including any top-level `await`, runs on every call and inside
|
|
118
|
+
that call's limits; the engine's built-in copy, by contrast, was evaluated once when the engine was
|
|
119
|
+
built, which is one reason it stays at 7.6.0.
|
|
120
|
+
|
|
121
|
+
**What the platform gives your function.** `fetch`, `URL`, `URLSearchParams`, `TextEncoder`,
|
|
122
|
+
`TextDecoder`, `AbortController`, `AbortSignal.timeout`, `setTimeout`, `setInterval`,
|
|
123
|
+
`crypto.randomUUID`, `crypto.getRandomValues`, `atob`, `btoa`, `console`, `Date` and `Math.random`
|
|
124
|
+
all work. `fetch` reaches only the platform API, through the SDK, and the save links the platform
|
|
125
|
+
hands you; any other URL is refused before anything is sent, and every request counts toward the
|
|
126
|
+
call's platform-call limit. Timers and `AbortSignal.timeout` wait their full length, and the engine
|
|
127
|
+
sets no limit of its own: what bounds a wait is the call's wall clock, and a call still waiting when
|
|
128
|
+
it runs out fails with `function_timeout`. A wait spends wall clock, not CPU.
|
|
129
|
+
|
|
130
|
+
**The SDK your function runs is the one you bundle.** A function published importing the SDK from
|
|
131
|
+
the engine runs the engine's built-in copy instead (the next paragraph). The starter's build
|
|
132
|
+
bundles the version your `package.json` names, so pin the version your game pins and the same code
|
|
133
|
+
runs in both places. A method added in a newer SDK reaches your function when you bundle that
|
|
134
|
+
version: update the pin, build, upload and promote.
|
|
135
|
+
|
|
136
|
+
**A function published importing the SDK from the engine** (the earlier starter marked it external)
|
|
137
|
+
keeps running on the engine's built-in copy, gem-web-sdk 7.6.0, which is not updated and is removed
|
|
138
|
+
once no published function imports it. Nothing fails at an engine roll. A new upload cannot import
|
|
139
|
+
it: the platform refuses every import, so a function moves off the built-in by a rebuild with the
|
|
140
|
+
current starter's build (which bundles the SDK), an upload of a new version and a promote. A method
|
|
141
|
+
added in a newer SDK reaches a function only when that version is bundled.
|
|
116
142
|
|
|
117
143
|
## 3. The SDK inside a function
|
|
118
144
|
|
|
@@ -120,8 +146,47 @@ function when the platform's engine picks that version up.
|
|
|
120
146
|
`awaitIdentity()` resolve at once, and `gem.me` is the player who called. So a game's boot sequence
|
|
121
147
|
runs unchanged, and the same `gem.me.saves.get`, `gem.leaderboards.get` and `gem.profiles.of` calls
|
|
122
148
|
work, with the same types and the same errors. The function's game and channel come from the call;
|
|
123
|
-
nothing you write names them.
|
|
124
|
-
|
|
149
|
+
nothing you write names them.
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
function createGem(options?: unknown): GemFunction; // options is accepted and ignored
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
| Member | Type | What it is |
|
|
156
|
+
| ----------------- | ------------------------------ | --------------------------------------------------------------------- |
|
|
157
|
+
| `context` | `FunctionContext` | Who called, for which game and channel, and this call's invocation id |
|
|
158
|
+
| `identity` | `GemFunctionIdentity \| null` | `playerId`, `gameId` and `channel`; `null` when a game server called |
|
|
159
|
+
| `awaitIdentity()` | `Promise<GemFunctionIdentity>` | `identity`, at once; rejects when a game server called |
|
|
160
|
+
| `awaitBridge()` | `Promise<'connected'>` | `'connected'`, at once |
|
|
161
|
+
| `me` | `EntityScope` | The calling player's settings, inventory, currencies and saves |
|
|
162
|
+
| `characters` | `CharactersApi` | The calling player's characters |
|
|
163
|
+
| `transfers` | `TransfersApi` | Moving items and currency between players |
|
|
164
|
+
| `analytics` | `AnalyticsApi` | Your game's analytics, as the calling player |
|
|
165
|
+
| `leaderboards` | `LeaderboardsApi` | The leaderboards your game config declares |
|
|
166
|
+
| `challenges` | `ChallengesApi` | Daily challenge periods and subjects |
|
|
167
|
+
| `showcase` | `ShowcaseApi` | Display-only values other players may read |
|
|
168
|
+
| `friends` | `FriendsApi` | The calling player's accepted friends |
|
|
169
|
+
| `platform` | `PlatformApi` | The calling player's platform-currency balances and earn allowance |
|
|
170
|
+
| `profiles` | `ProfilesApi` | Another player's per-game profile |
|
|
171
|
+
| `client` | `GemClient` | The platform API by path, for what the members above do not cover |
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
interface FunctionContext {
|
|
175
|
+
readonly gameId: string;
|
|
176
|
+
readonly channel: 'dev' | 'test' | 'release';
|
|
177
|
+
readonly invocationId: string; // the id `gemctl function invoke` reports
|
|
178
|
+
readonly caller:
|
|
179
|
+
| { readonly type: 'player'; readonly playerId: string }
|
|
180
|
+
| { readonly type: 'game-server'; readonly serverId: string | null };
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`gem.identity` is the same three fields a browser game reads from its own `gem.identity`, so code
|
|
185
|
+
that keys records on `playerId` runs unchanged. The entry also exports the error classes a game
|
|
186
|
+
already catches (`GemApiError`, `AuthError`, `TransportRefusal`, `GemSaveError` and
|
|
187
|
+
`FriendChallengeError`), so an `instanceof` check reads the same in both places. The
|
|
188
|
+
[function API reference](api/api-reference-function.md) documents each member above, and the
|
|
189
|
+
methods, types and errors behind all of them but `characters`, `transfers` and `client`.
|
|
125
190
|
|
|
126
191
|
What a browser game has that is backed by the page or the arcade (ads, purchases, rooms, device
|
|
127
192
|
settings, sharing) is not on `gem` inside a function. Four more differences, each following from
|
|
@@ -149,20 +214,29 @@ events may be lost.
|
|
|
149
214
|
## 4. Check before you upload
|
|
150
215
|
|
|
151
216
|
```bash
|
|
152
|
-
gemctl function check dist/function.js
|
|
217
|
+
gemctl function check functions/score-check/dist/function.js
|
|
153
218
|
```
|
|
154
219
|
|
|
155
220
|
`check` runs the platform's publish-time validation for JavaScript on your machine, and an upload
|
|
156
|
-
answers `400 source_invalid` with the same `details.reason`:
|
|
221
|
+
runs it first and answers `400 source_invalid` with the same `details.reason`:
|
|
157
222
|
|
|
158
|
-
| Reason | What it usually is
|
|
159
|
-
| ------------------ |
|
|
160
|
-
| `not_a_module` | A parse error, with its line and column, or a file that is not UTF-8
|
|
161
|
-
| `forbidden_import` |
|
|
162
|
-
| `missing_handle` | Neither `handle` nor `handleJson` is
|
|
163
|
-
| `ambiguous_handle` | Both are exported
|
|
164
|
-
| `too_large` | Over the size limit, the same one a component has
|
|
223
|
+
| Reason | What it usually is | What to change |
|
|
224
|
+
| ------------------ | ------------------------------------------------------------------------------------------- | ------------------------------------------------- |
|
|
225
|
+
| `not_a_module` | A parse error, with its line and column, or a file that is not UTF-8 | Fix the syntax; bundle to ES2022 |
|
|
226
|
+
| `forbidden_import` | Any `import` or `export … from`, or an `import()` | Bundle all of it in, the SDK too; drop `import()` |
|
|
227
|
+
| `missing_handle` | Neither `handle` nor `handleJson` is a named export (`export *` and `default` do not count) | Export one of them from the module itself |
|
|
228
|
+
| `ambiguous_handle` | Both are exported | Keep one |
|
|
229
|
+
| `too_large` | Over the size limit, the same one a component has | Bundle less; check what your build pulled in |
|
|
165
230
|
|
|
166
231
|
An upload also answers `source_invalid`, rather than `component_invalid`, for `not_uploaded`,
|
|
167
|
-
`size_mismatch` and `sha256_mismatch`: upload again.
|
|
168
|
-
|
|
232
|
+
`size_mismatch` and `sha256_mismatch`: upload again. gemctl uploads a `.js` or `.mjs` file as
|
|
233
|
+
JavaScript; it does not accept TypeScript, so upload what your build wrote.
|
|
234
|
+
|
|
235
|
+
## 5. Publishing
|
|
236
|
+
|
|
237
|
+
Uploading, promoting, rolling back and channels are the same as for a component
|
|
238
|
+
(serverless-functions.md sections 4 and 5). On the website, the **Functions** tab of your game's
|
|
239
|
+
**Builds** page promotes a version to a channel, rolls back by promoting an earlier one, and shows
|
|
240
|
+
each channel's calls and daily usage and the last 24 hours of `dev` logs; a JavaScript version is
|
|
241
|
+
marked there as running as JavaScript. Uploading is gemctl's alone. Invoking a function from your
|
|
242
|
+
game or the terminal is serverless-functions.md sections 6 and 7.
|
|
@@ -55,8 +55,10 @@ possible.
|
|
|
55
55
|
|
|
56
56
|
## 2. What a function is
|
|
57
57
|
|
|
58
|
-
A function is one WebAssembly **component** per game per channel
|
|
59
|
-
|
|
58
|
+
A function is one WebAssembly **component** or one JavaScript module per game per channel. A
|
|
59
|
+
JavaScript module is written against the SDK instead, and
|
|
60
|
+
[serverless-functions-js.md](serverless-functions-js.md) covers what differs for one. A component
|
|
61
|
+
is built against the platform's `gem:functions` world, and exports exactly one function:
|
|
60
62
|
|
|
61
63
|
```wit
|
|
62
64
|
export handle: func(input: list<u8>) -> result<list<u8>, string>;
|
|
@@ -450,12 +452,13 @@ reason to move the work to a game server.
|
|
|
450
452
|
| ------------------------------------------------------------------------------ | -------------------------------------- |
|
|
451
453
|
| Wall clock | 5 seconds |
|
|
452
454
|
| CPU | 1 second of that 5 |
|
|
453
|
-
| Memory |
|
|
455
|
+
| Memory | 256 MiB |
|
|
454
456
|
| `call-api`, `download-link` and `upload-link` calls | 32, all three together |
|
|
455
457
|
| A value handed to one `log`, `call-api`, `download-link` or `upload-link` call | 16 MiB |
|
|
456
458
|
| Input | 256 KiB |
|
|
457
459
|
| Output | 1 MiB |
|
|
458
|
-
|
|
|
460
|
+
| Lines your function logs, kept per call (`dev` only) | 100 lines or 64 KiB, whichever first |
|
|
461
|
+
| Component or JavaScript module size | 32 MiB |
|
|
459
462
|
| Calls, per caller per minute | 30 for a player, 120 for a game server |
|
|
460
463
|
|
|
461
464
|
**The per-minute budget belongs to the caller, not to your game.** A player who spends theirs is
|
|
@@ -464,8 +467,9 @@ decision point, such as the end of a run or a claim, and never on a timer or onc
|
|
|
464
467
|
|
|
465
468
|
## 3. Writing one
|
|
466
469
|
|
|
467
|
-
The reference toolchain is Rust
|
|
468
|
-
|
|
470
|
+
The reference toolchain for a component is Rust; to write a function in JavaScript or TypeScript,
|
|
471
|
+
see [serverless-functions-js.md](serverless-functions-js.md). gemctl writes the starter crate and
|
|
472
|
+
wraps what your build produces, and the build in between is yours:
|
|
469
473
|
|
|
470
474
|
```bash
|
|
471
475
|
gemctl function init functions/score-check
|
|
@@ -618,6 +622,15 @@ been purged yet.
|
|
|
618
622
|
`gemctl function delete <version> --yes` is refused while any channel serves the version, and
|
|
619
623
|
gemctl cannot undo it.
|
|
620
624
|
|
|
625
|
+
**The website does the same from the Functions tab of your game's Builds page.** It lists every
|
|
626
|
+
version with its description, size, SHA-256 and the channels serving it, and promotes, unassigns and
|
|
627
|
+
deletes, asking you to confirm before it promotes to `release`. Rolling back there is the same act:
|
|
628
|
+
promote the earlier version. It cannot upload; that is `gemctl function upload`. The tab also shows
|
|
629
|
+
each channel's calls today and its daily usage over the last 7, 28 or 90 days (calls, unsuccessful
|
|
630
|
+
calls, CPU time, the longest call and peak memory), and the last 24 hours of `dev` logs, filtered by
|
|
631
|
+
time, level, outcome or invocation id. Promoting, unassigning and deleting there take an Owner,
|
|
632
|
+
Admin or Contributor role on the game; anyone else on the team can only look.
|
|
633
|
+
|
|
621
634
|
## 5. Channels
|
|
622
635
|
|
|
623
636
|
Each channel (`dev`, `test` and `release`) serves at most one function version. A call never names
|
|
@@ -674,7 +687,8 @@ it is the progression's own state, or a key the platform decides (section 2).
|
|
|
674
687
|
platform's code from section 8, and the error adds what that code needs:
|
|
675
688
|
|
|
676
689
|
- `err.functionMessage` is the text your `handle` returned in `Err`, on `function_error`. Your
|
|
677
|
-
function wrote it and it may repeat what the caller sent, so log it and never render it as markup
|
|
690
|
+
function wrote it and it may repeat what the caller sent, so log it and never render it as markup.
|
|
691
|
+
For a JavaScript function it is the thrown exception's `name: message`;
|
|
678
692
|
- `err.failure` says which limit or fault ended the call, on `function_failed`;
|
|
679
693
|
- `err.retryable` is decided by the code. It is `false` for `function_timeout`, because the same
|
|
680
694
|
input runs out of time again, and for `functions_not_configured`, because functions are switched
|
|
@@ -777,9 +791,10 @@ or `game_server`:
|
|
|
777
791
|
exceeded. The platform writes it and never copies your function's data into it.
|
|
778
792
|
|
|
779
793
|
Newest first means a call's closing line comes before what it logged. **A call's logging is
|
|
780
|
-
capped**: once it reaches
|
|
781
|
-
unrecorded, and the closing line's `dropped_log_lines` says how many.
|
|
782
|
-
with a nonzero count means your function logged too much, not that it
|
|
794
|
+
capped**: once it reaches 100 lines or 64 KiB of logged text, whichever comes first, that `log`
|
|
795
|
+
call and every later one go unrecorded, and the closing line's `dropped_log_lines` says how many.
|
|
796
|
+
A log that stops part-way with a nonzero count means your function logged too much, not that it
|
|
797
|
+
went quiet.
|
|
783
798
|
|
|
784
799
|
`--invocation-id`, `--level` (`debug`, `info`, `warn` or `error`) and `--outcome` narrow the page.
|
|
785
800
|
`--level` picks what your function logged and `--outcome` picks closing lines, so the two cannot be
|
|
@@ -804,7 +819,7 @@ the last column: it is `true` exactly where this table says to retry.
|
|
|
804
819
|
|
|
805
820
|
| What you get | What happened | What to do |
|
|
806
821
|
| ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
807
|
-
| `422 function_error` | Your `handle` returned `Err
|
|
822
|
+
| `422 function_error` | Your `handle` returned `Err`, or your JavaScript function threw. The text is `err.functionMessage` | Fix your code or the input. Return verdicts in `Ok` |
|
|
808
823
|
| `422 function_failed` | It did not finish. `err.failure` is `trap`, `memory_exceeded`, `cpu_budget_exceeded`, `host_call_limit`, `output_too_large` or `argument_too_large` | A bug or a limit in the function. Reproduce with the same input |
|
|
809
824
|
| `504 function_timeout` | It ran past the wall clock | Do not retry: the same input takes as long again |
|
|
810
825
|
| `404 function_not_published` | Nothing is promoted on the caller's channel | `gemctl function promote <version> --channel <channel>` |
|
package/package.json
CHANGED
package/schema/bridge.v1.json
CHANGED
|
@@ -1514,6 +1514,90 @@
|
|
|
1514
1514
|
},
|
|
1515
1515
|
"rateLimit": { "perMinute": 30 }
|
|
1516
1516
|
},
|
|
1517
|
+
{
|
|
1518
|
+
"type": "gem.ticketEvent",
|
|
1519
|
+
"direction": "parentToFrame",
|
|
1520
|
+
"transport": "port",
|
|
1521
|
+
"$comment": [
|
|
1522
|
+
"A matchmaking ticket this player is on changed state. Gated by the",
|
|
1523
|
+
"`ticketEvents` capability; the mapping lives in schema/capabilities.json",
|
|
1524
|
+
"and nowhere else.",
|
|
1525
|
+
"",
|
|
1526
|
+
"gem-api's `matchmaking_ticket_queued`, `_matched`, `_expired`,",
|
|
1527
|
+
"`_cancelled` and `_failed`, which it sends to EVERY player a ticket",
|
|
1528
|
+
"seats: its owner and, for a party's ticket, every member. A party",
|
|
1529
|
+
"queues as one ticket and it is the leader's, so a member's frame is on a",
|
|
1530
|
+
"ticket it never created and cannot read — only the owner may. This is",
|
|
1531
|
+
"the only thing that can tell that frame its party is queued, and how",
|
|
1532
|
+
"the wait ended.",
|
|
1533
|
+
"",
|
|
1534
|
+
"A HINT, LIKE EVERY RELAY, and for a member a hint with nothing behind",
|
|
1535
|
+
"it to re-read. So the frame gives it exactly the authority of a",
|
|
1536
|
+
"display: a member's view of the ticket can be shown and cannot be",
|
|
1537
|
+
"cancelled or kept alive, and nothing is entered on the strength of it.",
|
|
1538
|
+
"A `matched` makes the frame look for the seat over its OWN token, which",
|
|
1539
|
+
"is where a room is believed. For the ticket's OWNER the event is a cue",
|
|
1540
|
+
"to ask the platform at once, exactly as a relayed match start is.",
|
|
1541
|
+
"",
|
|
1542
|
+
"ITS OWN TYPE, for the reason gem.seatedEvent gives: an unknown TYPE is",
|
|
1543
|
+
"ignored by any frame, and the capability keeps the parent from sending",
|
|
1544
|
+
"it to a frame that would not understand it.",
|
|
1545
|
+
"",
|
|
1546
|
+
"NOTHING ABOUT ANYBODY CROSSES. The push names no player and no other",
|
|
1547
|
+
"ticket, and neither does this. The templates gem-api lists on `queued`",
|
|
1548
|
+
"stop at the parent: the frame's game chose them, or its leader's did.",
|
|
1549
|
+
"",
|
|
1550
|
+
"`event` IS CLOSED, and it is the ticket's status. A status gem-api adds",
|
|
1551
|
+
"later is not relayed until this enum names it — the parent drops what",
|
|
1552
|
+
"it cannot name rather than send a value the frame would refuse.",
|
|
1553
|
+
"",
|
|
1554
|
+
"PARENT TO FRAME ONLY. A frame-authored ticket event would let a game",
|
|
1555
|
+
"tell the parent, and through it nobody, that a queue it does not own",
|
|
1556
|
+
"moved."
|
|
1557
|
+
],
|
|
1558
|
+
"fields": {
|
|
1559
|
+
"event": {
|
|
1560
|
+
"kind": "enum",
|
|
1561
|
+
"values": ["queued", "matched", "expired", "cancelled", "failed"],
|
|
1562
|
+
"$comment": "The ticket's status after the change. `queued` is its creation; every other value is final."
|
|
1563
|
+
},
|
|
1564
|
+
"ticketId": { "kind": "string", "maxLength": 64 },
|
|
1565
|
+
"partySize": {
|
|
1566
|
+
"kind": "int",
|
|
1567
|
+
"min": 1,
|
|
1568
|
+
"max": 64,
|
|
1569
|
+
"optional": true,
|
|
1570
|
+
"$comment": "How many players the ticket seats. Sent with `queued`."
|
|
1571
|
+
},
|
|
1572
|
+
"queuedAt": {
|
|
1573
|
+
"kind": "int",
|
|
1574
|
+
"min": 0,
|
|
1575
|
+
"max": 4102444800,
|
|
1576
|
+
"optional": true,
|
|
1577
|
+
"$comment": "When the ticket was queued, in Unix seconds. Sent with `queued`."
|
|
1578
|
+
},
|
|
1579
|
+
"expiresAt": {
|
|
1580
|
+
"kind": "int",
|
|
1581
|
+
"min": 0,
|
|
1582
|
+
"max": 4102444800,
|
|
1583
|
+
"optional": true,
|
|
1584
|
+
"$comment": "When the ticket gives up if it is still waiting, in Unix seconds. Sent with `queued`. A frame that has seen no ending once this long has passed stops showing the ticket as waiting: the pushes are not retried, and a member has no other way to learn the ticket is over."
|
|
1585
|
+
},
|
|
1586
|
+
"reason": {
|
|
1587
|
+
"kind": "string",
|
|
1588
|
+
"maxLength": 64,
|
|
1589
|
+
"optional": true,
|
|
1590
|
+
"$comment": "Why a ticket ended without a match: `timed_out`, `heartbeat_lapsed`, `requested`, `party_changed`, `handoff_failed`. Open, so a reason added later reaches the game."
|
|
1591
|
+
},
|
|
1592
|
+
"roomId": {
|
|
1593
|
+
"kind": "string",
|
|
1594
|
+
"maxLength": 64,
|
|
1595
|
+
"optional": true,
|
|
1596
|
+
"$comment": "The room a `matched` ticket's players were seated in. A place to look, not a seat: the frame reads the room over its own token and checks this player is a member."
|
|
1597
|
+
}
|
|
1598
|
+
},
|
|
1599
|
+
"rateLimit": { "perMinute": 60 }
|
|
1600
|
+
},
|
|
1517
1601
|
|
|
1518
1602
|
{
|
|
1519
1603
|
"type": "gem.signalEvent",
|