@indiegems/gem-web-sdk 13.0.0 → 13.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +4 -3
  3. package/dist/function.d.ts +2 -1
  4. package/dist/game.d.ts +37 -2
  5. package/dist/gem-web-sdk.bridge.js +1 -1
  6. package/dist/gem-web-sdk.esm.js +2 -2
  7. package/dist/gem-web-sdk.function.js +2 -2
  8. package/dist/gem-web-sdk.game.js +2 -2
  9. package/dist/gem-web-sdk.min.js +2 -2
  10. package/dist/gem-web-sdk.sdk.js +2 -2
  11. package/dist/gem-web-sdk.server-api.js +1 -1
  12. package/dist/gem-web-sdk.server.js +2 -2
  13. package/dist/index.d.ts +39 -5
  14. package/dist/sdk.d.ts +37 -2
  15. package/dist/server-api.d.ts +1 -0
  16. package/dist/server.d.ts +1 -0
  17. package/docs/analytics.md +6 -3
  18. package/docs/api/api-reference-function.md +38 -7
  19. package/docs/api/api-reference-game.md +381 -33
  20. package/docs/api/api-reference-server.md +23 -2
  21. package/docs/api/reference.json +401 -25
  22. package/docs/arcade-avatars.md +194 -26
  23. package/docs/brand-art.md +3 -3
  24. package/docs/changelog.md +30 -0
  25. package/docs/deprecation-policy.md +6 -0
  26. package/docs/getting-started.md +9 -5
  27. package/docs/inventory-and-catalogs.md +1 -1
  28. package/docs/manifest.json +29 -29
  29. package/docs/platform-moments.md +266 -0
  30. package/docs/rewarded-ads.md +26 -9
  31. package/docs/server-bundles.md +2 -2
  32. package/docs/serverless-functions-js.md +17 -1
  33. package/docs/serverless-functions.md +13 -7
  34. package/docs/what-a-game-must-do.md +126 -70
  35. package/package.json +6 -2
  36. package/rigs/adult_human.glb +0 -0
  37. package/schema/bridge.v1.json +160 -7
  38. package/schema/capabilities.public.json +16 -0
  39. package/schema/moment-log-line.json +109 -0
  40. package/docs/cross-promotion.md +0 -232
@@ -0,0 +1,266 @@
1
+ # Platform moments
2
+
3
+ At every natural break in play — a level ending, a run over, a match's results, a return to the
4
+ menu — your game signals a **platform moment**. The Arcade then decides, on its own servers,
5
+ whether to show one of **its own** offers over your paused game: a break video, the Arcade app, a
6
+ platform store offer, a subscription, another game. Usually it shows nothing.
7
+
8
+ ```js
9
+ // From the press that LEAVES the break — Next, Retry, Back to menu.
10
+ async function onNextPressed() {
11
+ startLevelTransition(); // your fade or loading beat, in parallel
12
+ const { shown } = await gem.moments.signal({ kind: 'level_end' });
13
+ startNextLevel(); // settled either way; never grant anything on `shown`
14
+ }
15
+ ```
16
+
17
+ **Every game signals a moment at every break**, whether or not it has a shop of its own. The
18
+ release checklist requires it (§11). That is the whole surface; the rest of this page is where to
19
+ put the call, where never to, and the few things a game gets wrong when nobody tells it.
20
+
21
+ - [1. A platform moment is not your own offer](#1-a-platform-moment-is-not-your-own-offer)
22
+ - [2. The five kinds](#2-the-five-kinds)
23
+ - [3. Where to call it, and where never to](#3-where-to-call-it-and-where-never-to)
24
+ - [4. The answer is usually `false`, and false is ordinary](#4-the-answer-is-usually-false-and-false-is-ordinary)
25
+ - [5. Nothing rejects](#5-nothing-rejects)
26
+ - [6. `shown` grants nothing](#6-shown-grants-nothing)
27
+ - [7. The Arcade pauses your game for you](#7-the-arcade-pauses-your-game-for-you)
28
+ - [8. Multiplayer](#8-multiplayer)
29
+ - [9. What never crosses](#9-what-never-crosses)
30
+ - [10. Testing it in the dev host](#10-testing-it-in-the-dev-host)
31
+ - [11. The release requirement](#11-the-release-requirement)
32
+ - [12. Moving off `format: 'interstitial'`](#12-moving-off-format-interstitial)
33
+
34
+ ## 1. A platform moment is not your own offer
35
+
36
+ Two kinds of upsell can happen in a game, and they never mix.
37
+
38
+ | | **Platform moment** (this page) | **Your game's own offer** |
39
+ | ------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
40
+ | Who decides | the Arcade, on its servers | your game |
41
+ | What it shows | platform things only: break videos, the Arcade app, platform store offers, subscriptions, other games | your own items, currencies and rewarded buttons |
42
+ | When | only at a moment your game signals, paced by the platform | whenever your game likes, on its own cadence |
43
+ | Drawn by | the Arcade's own chrome, over your paused game | your game's own UI |
44
+ | What your game learns | `shown: true` or `false`, nothing else | everything — it is yours |
45
+
46
+ Three rules follow from it:
47
+
48
+ - **A platform moment never shows your game's own items, and your game never routes its own offer
49
+ through one.** Your shop, your special offers and your rewarded buttons
50
+ ([`rewarded-ads.md`](rewarded-ads.md)) stay yours, in your UI, on your schedule.
51
+ - **Signal every natural break, whether or not your game has a shop of its own.** The moment is
52
+ about the break, not about what your game sells.
53
+ - **No stacking.** When a moment settles `shown: true`, do not also pop your own offer at that
54
+ break. Your normal results screen and its shop button are fine; a second popup in the same breath
55
+ is not.
56
+
57
+ ## 2. The five kinds
58
+
59
+ `kind` is required, and it is one of five. The set is closed and will not grow: a game written
60
+ today never meets a sixth.
61
+
62
+ | `kind` | the break | for example |
63
+ | ------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
64
+ | `level_end` | a level, stage, wave or puzzle finished, won or lost | a platformer's stage clear, a puzzle solved, a tower-defence wave beaten |
65
+ | `run_end` | game over, death, or a run banked | an endless runner's crash, a roguelike death, an arcade game's last life |
66
+ | `match_end` | a multiplayer round or match finished, and the player is leaving its results | a battle royale's results, a card game's round, a race's podium |
67
+ | `menu_return` | the player chose to go back to the title, a main menu or level select — a return, never the first arrival | Back to menu on a results screen, Quit to menu from the pause menu |
68
+ | `other_break` | any other natural stop | the end of a story chapter, a between-waves shop |
69
+
70
+ `kind` is a **label**. The Arcade uses it for its own analytics, the dev host prints it (§10), and
71
+ the release checklist reads it (§11). It never changes what the Arcade decides for your game, so
72
+ there is nothing to gain by choosing one kind over another: pick the one that describes the break.
73
+
74
+ `placement` is optional: your own name for the screen the player is leaving — `world-1-3`,
75
+ `results`, `pause_quit`. It has the grammar every name you hand the SDK has, **1 to 32 characters of
76
+ `a-z`, `0-9`, `-` and `_`**, and it appears in exactly one place: the dev host's log, so a reviewer
77
+ can map each moment to a screen. It is never sent to the platform, never stored, never shown to a
78
+ player and never sent to an ad network.
79
+
80
+ ```js
81
+ await gem.moments.signal({ kind: 'run_end', placement: 'desert-run' });
82
+ ```
83
+
84
+ ## 3. Where to call it, and where never to
85
+
86
+ **From the player's press that leaves the break** — Next, Retry, Continue, Back to menu. The
87
+ results screen stays fully interactive until then: the player reads their score, looks at the
88
+ leaderboard, opens your shop. When they press to leave, your game starts its own transition (a fade,
89
+ a loading beat) and signals at the same moment, in parallel, and starts play when the promise
90
+ settles. Usually nothing was shown and the answer arrives inside your transition, so the player
91
+ never sees a wait.
92
+
93
+ Once per break. A break with three ways out signals from whichever press the player made, not from
94
+ all three.
95
+
96
+ **Never:**
97
+
98
+ - mid-input, or during a timed sequence — a jump, a countdown, a combo;
99
+ - on a timer, or on a schedule of your own;
100
+ - on boot, or on the first arrival at the title screen — a `menu_return` is a **return**;
101
+ - from a pause menu the player opened, just because it opened — choosing **Quit to menu** from it
102
+ **is** a `menu_return`, signalled from that press;
103
+ - more than once at one break.
104
+
105
+ No press is required by the SDK: the rule is about when a break really is one, not about user
106
+ activation.
107
+
108
+ ## 4. The answer is usually `false`, and false is ordinary
109
+
110
+ The promise resolves `{ shown }`, one boolean:
111
+
112
+ | `shown` | what happened | what your game does |
113
+ | ------- | ---------------------------------------------------------------- | ----------------------------------------- |
114
+ | `false` | nothing was shown | carry on exactly as if you had not asked |
115
+ | `true` | the Arcade showed something of its own, and it has closed | carry on — and grant nothing (§6) |
116
+
117
+ **`false` is the common answer**, not an edge case. The Arcade paces what it shows, gives a new
118
+ player time before it shows anything, and has nothing suitable more often than not. You are not told
119
+ which, and there is nothing to infer.
120
+
121
+ - Do **not** render a placeholder, a spinner or a "nothing right now" message.
122
+ - Do **not** hold your game's flow on a fill. Start your transition, signal, and continue when the
123
+ promise settles.
124
+ - Do **not** retry on a `false`. A second signal at the same break is refused (§5).
125
+
126
+ ## 5. Nothing rejects
127
+
128
+ `gem.moments.signal` always resolves. There is no error branch and no `try`/`catch` to write. When
129
+ the SDK refuses a signal itself, it resolves `{ shown: false }` without asking the Arcade, and calls
130
+ your `onError` handler with code `'internal'` and a message that **begins with a stable token**, so
131
+ you find the mistake while you build:
132
+
133
+ | token | when |
134
+ | --------------------------- | -------------------------------------------------------------------------------------------------- |
135
+ | `moments_unsupported` | the page your game runs in does not do platform moments. Keep the call; it costs nothing. Said once a session |
136
+ | `moments_invalid_argument` | a `kind` outside the five, or a `placement` outside the grammar |
137
+ | `moments_before_ready` | a signal before `gem.frame.ready()` — play has not started, so there is no break |
138
+ | `moments_in_flight` | a second signal while the first is still waiting for its answer |
139
+ | `moments_called_repeatedly` | a signal within 10 seconds of the last moment's answer — one break signalled twice, or a timer |
140
+ | `moments_timeout` | no answer within three minutes |
141
+
142
+ The 10 seconds count from the last **answer**, not the last call, so a long panel does not eat into
143
+ the next level. A refused signal does not restart the count.
144
+
145
+ Match on the token, not the rest of the message:
146
+
147
+ ```js
148
+ const gem = createGem({
149
+ onError: (code, message) => {
150
+ if (message?.startsWith('moments_') && !message.startsWith('moments_unsupported')) {
151
+ reportBug('platform moment refused', message); // your own telemetry
152
+ }
153
+ },
154
+ });
155
+ ```
156
+
157
+ ## 6. `shown` grants nothing
158
+
159
+ `shown: true` says the Arcade showed something of its own and it has closed. It is **not** a
160
+ receipt, **not** an entitlement, and **not** permission to hand anything out. Nothing on this call
161
+ pays the player, and a game that unlocks a level or grants a currency on the strength of it has an
162
+ unlock any page embedding your game can forge.
163
+
164
+ If your game wants to give the player something for reaching the end of a level, give it to them
165
+ for reaching the end of the level.
166
+
167
+ ## 7. The Arcade pauses your game for you
168
+
169
+ While the Arcade shows something, your game is paused the same way it is when a player switches
170
+ tabs — the Arcade sends the pause your game already handles, and lifts it when the panel closes.
171
+ You do not pause anything yourself, and you never resume on a timer: resume when the promise
172
+ settles. If you have not wired the pause yet, that is the thing to fix first;
173
+ [`what-a-game-must-do.md`](what-a-game-must-do.md) covers it.
174
+
175
+ ## 8. Multiplayer
176
+
177
+ Signal `match_end` as the player leaves a match's results, exactly as a single-player game signals
178
+ `level_end`. What the Arcade may show to a player who is still in a live room or a party about to
179
+ start is the Arcade's decision, made from its own knowledge of the room — never from the `kind`
180
+ your game sent — so there is nothing extra for your game to do, and nothing to hold back. The
181
+ Arcade does not keep a player who is about to play waiting on an offer.
182
+
183
+ ## 9. What never crosses
184
+
185
+ Your game never learns what was shown — no title, no image, no offer, no id. It receives one
186
+ boolean.
187
+
188
+ You are also never told **why** nothing was shown. That is deliberate and permanent: the reasons
189
+ include pacing, a new player's grace, the player's account, age and consent, and a reason would
190
+ tell your game something about the player that game code is not told. There will not be a second
191
+ field naming one. The Arcade also settles every `false` on a schedule that does not depend on why,
192
+ so there is nothing in the timing to read either. (`true` is not held back — how long that one
193
+ takes is how long your player spent with a panel.)
194
+
195
+ ## 10. Testing it in the dev host
196
+
197
+ `gemctl serve` and the Gem Editor's dev host answer moments the way the Arcade does, with a TEST
198
+ panel in place of a real offer. (Both dev hosts gain this in their own releases after the SDK that
199
+ ships `gem.moments`. Until yours has it, a moment there answers `false`, and your `onError`
200
+ handler hears `moments_unsupported` once.)
201
+
202
+ > **Platform moment · level_end · world-1-3**
203
+ > In the Gem Arcade, an offer of the Arcade's own may appear here. Usually nothing does. Your game
204
+ > is paused until this closes.
205
+ > **[Close]**
206
+
207
+ Add `?moments=` to the dev host's page to choose what it does:
208
+
209
+ | mode | what the dev host does |
210
+ | --------- | ----------------------------------------------------------------------------------------- |
211
+ | `none` | shows nothing, and answers `false` after the same short wait the Arcade holds |
212
+ | `panel` | shows the TEST panel at every moment |
213
+ | `paced` | shows the panel at most once every three minutes, so you see the real frequency |
214
+ | `video` | the longest path: a ten-second break video, then its result |
215
+ | `delayed` | paints the panel late, to test a player who presses Continue before it arrives |
216
+
217
+ Each moment writes **one log line**, in the same form on every dev host:
218
+
219
+ ```text
220
+ moment kind=level_end placement=world-1-3 since_ready_ms=84210 since_prev_ms=61005 answer=not_shown
221
+ ```
222
+
223
+ `since_ready_ms` is how long after `gem.frame.ready()` the moment arrived, `since_prev_ms` how long
224
+ after the previous one (`-` for the first), and `answer` is `shown` or `not_shown`. A
225
+ `placement` you did not give is `-`, and a deprecated interstitial ask is logged on the same line as
226
+ `kind=legacy`. The grammar of the line is published with the SDK, as
227
+ `@indiegems/gem-web-sdk/schema/moment-log-line.json`.
228
+
229
+ Play through every break in your game once and read the log: every results, game-over and
230
+ back-to-menu screen should have produced a line, with the `kind` you meant, and none should appear
231
+ on boot, mid-level or seconds apart. That log is what the release checklist reads (§11).
232
+
233
+ ## 11. The release requirement
234
+
235
+ The release checklist requires platform moments, as a platform requirement every game meets:
236
+
237
+ - **MOM-01 — every natural break signals a platform moment.** The checklist inventories your
238
+ breaks from your code and from a play-through under `gemctl serve`, and expects a log line for
239
+ each. A game with no moments at all does not pass; a break with no signal is a finding to fix.
240
+ - **MOM-02 — never at the wrong time.** No signal right after `ready()`, none on a regular
241
+ interval, none twice at one break, none mid-input and none from opening a pause menu. The log
242
+ line is how the first three are checked.
243
+ - **MOM-03 — no stacking** (advisory): no popup of your own at a break that settled
244
+ `shown: true`.
245
+
246
+ **The platform itself does not enforce any of this.** Publishing never refuses a game for missing
247
+ moments, the Arcade never refuses to run one, and `gemctl`'s doctor warns rather than fails. The
248
+ checklist is where it is required, and it is required there at every important break.
249
+
250
+ ## 12. Moving off `format: 'interstitial'`
251
+
252
+ `gem.ads.requestAd({ placement, format: 'interstitial' })` is **deprecated**. It still works, and
253
+ the Arcade treats it as a platform moment, but `14.0.0` is the earliest release that may remove it
254
+ ([`deprecation-policy.md`](deprecation-policy.md)). Replace it at the same break, and move the call
255
+ to the press that leaves the break if it was made as the results screen opened:
256
+
257
+ ```js
258
+ // Before
259
+ await gem.ads.requestAd({ placement: 'level-complete', format: 'interstitial' });
260
+
261
+ // After — from the Next press
262
+ await gem.moments.signal({ kind: 'level_end', placement: 'level-complete' });
263
+ ```
264
+
265
+ `format: 'rewarded'` is not deprecated and does not change: a button the player presses for a
266
+ reward is your game's own offer, and [`rewarded-ads.md`](rewarded-ads.md) is its page.
@@ -204,10 +204,11 @@ cannot tell which input caused the ask: a browser counts any tap in your game fo
204
204
  and the SDK counts any controller input for one, so an ask from your jump button's handler, or
205
205
  from a death a second after a tap, goes through.
206
206
  Making the ask only from the button is yours to get right.
207
- [`cross-promotion.md`](cross-promotion.md#where-to-put-the-call) has the handler.
208
207
 
209
- The answer is one of the same three outcomes every `requestAd` gets — `shown`, `no_ad` or
210
- `unavailable` — and for `rewarded` none of them says whether a reward was granted. What can happen
208
+ The answer is one of three outcomes — `shown`, `no_ad` or `unavailable`, the last two read
209
+ identically — and it never rejects: even a `placement` outside the grammar resolves `no_ad`, and
210
+ the mistake goes to your `onError` handler. For `rewarded` none of the three says whether a reward
211
+ was granted. What can happen
211
212
  after a press:
212
213
 
213
214
  | what happened | was the reward granted? |
@@ -230,7 +231,7 @@ player already holds before asking for another (§5's code does both).
230
231
  A granted reward shows in your order history like any other change, as an order the platform made
231
232
  with `source` `reward`.
232
233
 
233
- **While the ad plays, your game is paused,** the same way it is for an interstitial: the Arcade
234
+ **While the ad plays, your game is paused,** the same way it is at a [platform moment](platform-moments.md): the Arcade
234
235
  sends the pause your game already handles and lifts it when the ad closes. Resume when the promise
235
236
  settles, not on a timer. A second `requestAd` while one is still in flight resolves `no_ad` at once,
236
237
  so disable the button between the press and the answer.
@@ -301,13 +302,28 @@ a cap is spent, a cooldown runs, the player holds `max_held`, or rewarded is off
301
302
  drawing on it never offers a press that is certain to pay nothing. Hide or grey the button out
302
303
  otherwise, and say when it is back from `nextEligibleAt` or `resetsAt`. `grantable` is not a
303
304
  promise: a press can still pay nothing for reasons the read never reports. The status reserves
304
- nothing, and it never rejects. See [cross-promotion.md](cross-promotion.md):
305
+ nothing, and it never rejects:
305
306
 
306
307
  ```js
307
308
  const status = await gem.ads.rewardedStatus('classic_revive');
308
309
  watchAdButton.hidden = !status.grantable;
309
310
  ```
310
311
 
312
+ | field | what it says |
313
+ | ------------------ | -------------------------------------------------------------------------- |
314
+ | `declared` | the config promoted to the channel being played names this placement |
315
+ | `grantable` | pressing the button could pay right now — **not** that an ad will play |
316
+ | `remainingToday` | grants left today under the daily cap; absent when the placement has none |
317
+ | `nextEligibleAt` | when a running cooldown ends, in epoch milliseconds; absent when none runs |
318
+ | `heldLimitReached` | the player already holds the placement's `max_held` — spend one first |
319
+ | `resetsAt` | when today's count resets, the next 00:00 UTC, in epoch milliseconds |
320
+
321
+ Without a play token, when the read fails or takes longer than ten seconds, or for a placement
322
+ outside the grammar, it answers not declared and not grantable with no `resetsAt`, and the reason
323
+ goes to your `onError` handler. It is a read of the platform, so call it when you draw the button
324
+ — on a results or death screen — not every frame, and draw the rest of the screen first rather than
325
+ holding it on the answer.
326
+
311
327
  For a revive, the screen reads what it needs **when it opens**, so the press itself can ask at
312
328
  once:
313
329
 
@@ -437,10 +453,11 @@ platform sets them all. A rewarded reward is your economy, so the caps are yours
437
453
 
438
454
  ## 7. Pacing
439
455
 
440
- - **A rewarded ask is never held back by an interstitial.** A player who presses your button is not
441
- refused because a promotion played a minute ago, in your game or another.
442
- - **An ad that starts holds off the next interstitial.** Do not follow a rewarded ad with an
443
- interstitial at the same break; that ask answers `no_ad`.
456
+ - **A rewarded ask is never held back by a platform moment.** A player who presses your button is
457
+ not refused because the Arcade showed something a minute ago, in your game or another.
458
+ - **A rewarded ad does not replace the break's platform moment.** Signal the moment from the press
459
+ that leaves the break as usual ([`platform-moments.md`](platform-moments.md)); whether the Arcade
460
+ shows anything then is its decision.
444
461
  - **Rewarded has its own spacing:** your placement's `cooldown_seconds`, and the platform's 15
445
462
  seconds (§6). An ask inside any of them answers `no_ad` with nothing granted, so do not re-ask on
446
463
  a timer. Never ask on a timer at all: the ad would start with no press. The SDK refuses a timer's
@@ -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/v13.0.0/gem-web-sdk.tgz
84
- curl -fsSL https://downloads.gemarcade.com/sdk/v13.0.0/gem-web-sdk.tgz.sri
83
+ curl -fsSL -O https://downloads.gemarcade.com/sdk/v13.2.0/gem-web-sdk.tgz
84
+ curl -fsSL https://downloads.gemarcade.com/sdk/v13.2.0/gem-web-sdk.tgz.sri
85
85
  openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
86
86
  ```
87
87
 
@@ -1,5 +1,16 @@
1
1
  # Serverless functions in JavaScript
2
2
 
3
+ ⚠️ **JavaScript functions are disabled for now, due to performance issues. Write a new function in
4
+ Rust.** A JavaScript function runs many times slower on the platform than the same function built
5
+ from Rust as a WebAssembly component, too slow to fit a call's CPU limit, so uploading a new
6
+ JavaScript function is refused with `400 js_functions_disabled`: "New JavaScript functions are not
7
+ accepted: build the function as a WebAssembly component from Rust (the gem:functions world, target
8
+ wasm32-unknown-unknown) and finalize it with kind `component`. Versions already published as
9
+ JavaScript keep running." [serverless-functions.md](serverless-functions.md#3-writing-one) shows how
10
+ to write one in Rust. A JavaScript function you have already published keeps running: it is still
11
+ listed, you can still promote it to a channel or roll back to it, and players' calls still reach
12
+ it. This page stays for those functions, and for when JavaScript functions come back.
13
+
3
14
  A serverless function can be written in JavaScript or TypeScript instead of Rust: the language your
4
15
  game is already written in, so the verification code in your browser game moves into a function
5
16
  with no change beyond bundling. You upload one bundled ES module, and the platform runs it on a
@@ -218,7 +229,9 @@ gemctl function check functions/score-check/dist/function.js
218
229
  ```
219
230
 
220
231
  `check` runs the platform's publish-time validation for JavaScript on your machine, and an upload
221
- runs it first and answers `400 source_invalid` with the same `details.reason`:
232
+ runs it first and answers `400 source_invalid` with the same `details.reason`. While JavaScript
233
+ functions are disabled (the notice at the top of this page), an upload is refused with
234
+ `400 js_functions_disabled` before any of these checks run.
222
235
 
223
236
  | Reason | What it usually is | What to change |
224
237
  | ------------------ | ------------------------------------------------------------------------------------------- | ------------------------------------------------- |
@@ -234,6 +247,9 @@ JavaScript; it does not accept TypeScript, so upload what your build wrote.
234
247
 
235
248
  ## 5. Publishing
236
249
 
250
+ While JavaScript functions are disabled, a new JavaScript version cannot be uploaded; promoting a
251
+ version you already published, and rolling back to one, work as before.
252
+
237
253
  Uploading, promoting, rolling back and channels are the same as for a component
238
254
  (serverless-functions.md sections 4 and 5). On the website, the **Functions** tab of your game's
239
255
  **Builds** page promotes a version to a channel, rolls back by promoting an earlier one, and shows
@@ -55,10 +55,15 @@ possible.
55
55
 
56
56
  ## 2. What a function is
57
57
 
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:
58
+ A function is one WebAssembly **component** per game per channel, built from Rust.
59
+
60
+ ⚠️ **JavaScript functions are disabled for now, due to performance issues. Write your function in
61
+ Rust.** Uploading a new JavaScript function is refused with `400 js_functions_disabled`. A
62
+ JavaScript function you have already published keeps running, and
63
+ [serverless-functions-js.md](serverless-functions-js.md) still covers it.
64
+
65
+ A component is built against the platform's `gem:functions` world, and exports exactly one
66
+ function:
62
67
 
63
68
  ```wit
64
69
  export handle: func(input: list<u8>) -> result<list<u8>, string>;
@@ -467,9 +472,9 @@ decision point, such as the end of a run or a claim, and never on a timer or onc
467
472
 
468
473
  ## 3. Writing one
469
474
 
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:
475
+ A function is written in Rust and built into a component; JavaScript functions are disabled for
476
+ now, due to performance issues (section 2). gemctl writes the starter crate and wraps what your
477
+ build produces, and the build in between is yours:
473
478
 
474
479
  ```bash
475
480
  gemctl function init functions/score-check
@@ -844,6 +849,7 @@ Publishing has its own refusals, and nothing is stored when one happens:
844
849
  | What you get | What happened | What to do |
845
850
  | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
846
851
  | `400 component_invalid` | The backend's check refused the component. `details.reason` is one of the five in section 3, or `sha256_mismatch`, `size_mismatch` or `not_uploaded` | The five: fix and rebuild. The other three: upload again |
852
+ | `400 js_functions_disabled` | You uploaded a JavaScript function. JavaScript functions are disabled for now, due to performance issues; versions already published as JavaScript keep running | Write it in Rust and upload the component (section 3) |
847
853
  | `409 function_version_exists` | That version string is taken, perhaps by a deleted version not yet purged | Choose another version |
848
854
  | `409 function_version_in_use` | You tried to delete a version a channel still serves | Promote another version there, or unassign it first |
849
855
  | `404 function_version_not_found` | No such version, or it has been deleted | `gemctl function list --all --include-deleted` |