@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/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.
@@ -50,7 +50,7 @@ either way.
50
50
  ```jsonc
51
51
  // package.json
52
52
  "dependencies": {
53
- "@indiegems/gem-web-sdk": "11.1.2"
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`
@@ -33,8 +33,8 @@
33
33
  },
34
34
  {
35
35
  "file": "changelog.md",
36
- "sha256": "f88799ff04404b0ad76c5774375a66772c9ed7a64b59ca566f473f1302c9b2d7",
37
- "updated": "2026-10-12T17:30:48Z"
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": "4a824bccf87d75c2884a7159b44c768b39096f815b80fa7b5018da2862add89e",
72
- "updated": "2026-10-12T17:30:48Z"
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": "971e67d9cb74a046cddc0ff76cdea64f1aef3af8e9376944388ca8aa87730729",
117
- "updated": "2026-10-12T02:26:43Z"
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": "f1ff81f3ee1950dae0d8b0fab66994eeec99d6e6bf1684b121c742f29d63e0b9",
127
- "updated": "2026-10-12T17:30:48Z"
126
+ "sha256": "52054ceeb56b7130bfaf3461c2d81a9bd3b3dcd0f95097753ebfffc08f7a1cbb",
127
+ "updated": "2026-10-12T22:12:20Z"
128
128
  },
129
129
  {
130
130
  "file": "serverless-functions-js.md",
131
- "sha256": "56588d50d5a59f066164565470663f8f9da03fe85d98a96d3754e6ec14eb9fac",
132
- "updated": "2026-10-10T00:12:33Z"
131
+ "sha256": "3904704418f9c6c246c8e1fa83f96901f6e493a3f4499b16fd6845620515e57d",
132
+ "updated": "2026-10-12T17:39:05Z"
133
133
  },
134
134
  {
135
135
  "file": "serverless-functions.md",
136
- "sha256": "b1562b4442b69bb6895198def7b00ccd7a8c56133862a939c2cce9b6e9d30290",
137
- "updated": "2026-10-07T19:27:16Z"
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`:
@@ -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/v11.1.2/gem-web-sdk.tgz
84
- curl -fsSL https://downloads.gemarcade.com/sdk/v11.1.2/gem-web-sdk.tgz.sri
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. Exporting neither, or both, is refused
55
- when you upload. A caller cannot tell which kind of function answered: `gem.functions.invoke`, the
56
- limits and the refusals in serverless-functions.md are identical for both.
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 go to the platform's log for the invocation, never to the caller. A
61
- `handleJson` whose input is not JSON, or whose return value JSON cannot carry, fails the same way.
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, a TypeScript starter handler and a `tsconfig.json`. gemctl runs neither step:
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 may import the SDK and nothing else, under any of the three names a game
100
- already uses: `@indiegems/gem-web-sdk/function`, `@indiegems/gem-web-sdk/sdk`, or
101
- `@indiegems/gem-web-sdk/game`. Inside a function all three are the same module, the function
102
- entry, so shared code keeps its import. A name that module does not export, such as
103
- `defineServer`, fails every call with `function_error`. Everything else your code needs is bundled
104
- in, which is what the starter's build does. An `import()` at runtime is refused at upload.
105
- Top-level `await` is allowed and runs on every call.
106
-
107
- **What the engine gives you.** `Date`, `performance`, `Math.random`, `crypto.getRandomValues`,
108
- `crypto.randomUUID`, `TextEncoder`, `URL` and `fetch` all work. `fetch` reaches only the platform
109
- API, through the SDK, and the save links the platform hands you; any other URL is refused. A timer
110
- waits at most 10 ms: a function has no reason to sleep, and a long one would run out the wall
111
- clock.
112
-
113
- **The SDK version your function runs is the one the platform bundles**, not the one in your
114
- `package.json`; the starter pins the matching version. A method added in a newer SDK reaches a
115
- function when the platform's engine picks that version up.
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. `gem.context` is who called, for which game and channel, and
124
- `gem.identity` is the same three fields a browser game reads from its own `gem.identity`.
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 | What to change |
159
- | ------------------ | ------------------------------------------------------------------------- | ------------------------------------------------- |
160
- | `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 |
161
- | `forbidden_import` | An `import` or `export … from` of anything but the SDK, or an `import()` | Bundle the dependency in; drop the dynamic import |
162
- | `missing_handle` | Neither `handle` nor `handleJson` is exported (`export *` does not count) | Export one of them from the module itself |
163
- | `ambiguous_handle` | Both are exported | Keep one |
164
- | `too_large` | Over the size limit, the same one a component has | Bundle less; check what your build pulled in |
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. Promoting, channels and invoking are the same
168
- as for a component (serverless-functions.md sections 4 to 7).
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, built against the platform's
59
- `gem:functions` world. It exports exactly one function:
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 | 64 MiB |
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
- | Component size | 32 MiB |
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. gemctl writes the starter crate and wraps what your build
468
- produces, and the build in between is yours:
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 its per-call log limit, that `log` call and every later one go
781
- unrecorded, and the closing line's `dropped_log_lines` says how many. A log that stops part-way
782
- with a nonzero count means your function logged too much, not that it went quiet.
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`. The text is `err.functionMessage` | Fix your code or the input. Return verdicts in `Ok` |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indiegems/gem-web-sdk",
3
- "version": "11.1.2",
3
+ "version": "12.0.0",
4
4
  "description": "postMessage bridge contract between gem-web and browser-playable games",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -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",
@@ -93,6 +93,12 @@
93
93
  "gem.autoStartFailedEvent"
94
94
  ]
95
95
  },
96
+ "ticketEvents": {
97
+ "implemented": true,
98
+ "gates": [
99
+ "gem.ticketEvent"
100
+ ]
101
+ },
96
102
  "partySeating": {
97
103
  "implemented": true,
98
104
  "gates": [