@indiegems/gem-web-sdk 7.9.0 → 8.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,105 @@ 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
+ ## 8.0.0
20
+
21
+ *Written from the API facts alone; no summary was generated for this release.*
22
+
23
+ Upgrading from 7.9.1. Impact: required.
24
+
25
+ ### Must change
26
+
27
+ - BucketConfig changed its signature.
28
+ - FRAME_TO_PARENT changed its signature.
29
+ - FrameMessage changed its signature.
30
+ - GemCapability changed its signature.
31
+
32
+ ### New capability
33
+
34
+ - FriendChallenge was added.
35
+ - FriendChallengesPage was added.
36
+ - FriendsApi.inbox was added.
37
+ - FriendsApi.launchedChallenge was added.
38
+ - FriendsApi.markAnswered was added.
39
+ - FriendsApiDependencies.launchedChallengeId was added.
40
+ - ListFriendChallengesOptions was added.
41
+ - Gem.vibration was added.
42
+ - GemFrame.requestVibrate was added.
43
+ - VibrationApi was added.
44
+ - GemVibrate was added.
45
+ - HostCallbacks.onVibrate was added.
46
+ - HostOptions.onVibrate was added.
47
+ - validateGemVibrate was added.
48
+
49
+ ## 7.9.1
50
+
51
+ Upgrading from 7.9.0. Impact: none.
52
+
53
+ Nothing in this release asks anything of a game.
54
+
55
+ ## 7.9.0
56
+
57
+ Upgrading from 7.8.1. Impact: required.
58
+
59
+ ### Must change
60
+
61
+ - A link your game shares can no longer carry a parameter named build. The Arcade now reads that key for itself and removes it before your game starts, so the share request refuses it with a TypeError that names the key. Rename the parameter if your game shares one by that name.
62
+
63
+ ### Rebuild
64
+
65
+ - GemCapability gained gameMenu, and the bridge's message lists and unions in both directions gained the two new menu messages. A switch that assumes it knows every GemCapability value must handle the new one. Other code that compiles today keeps compiling.
66
+
67
+ ### New capability
68
+
69
+ - gem.setMenu adds to the Arcade's menu, the one control the Arcade keeps over a game in its top right corner. Give onSettings and the menu gains a settings entry that opens your own settings screen, and give items for up to four entries of your own, each with an id, a label you have already translated and an onSelect handler. Each call is the whole answer and replaces the last, so call it again when the entries change. It returns true when the Arcade was told. After awaitBridge has settled, draw a settings button of your own only when it returns false. The Arcade pauses your game while its menu is open and resumes it before your handler runs.
70
+
71
+ ## 7.8.1
72
+
73
+ Upgrading from 7.8.0. Impact: none.
74
+
75
+ Nothing in this release asks anything of a game.
76
+
77
+ ## 7.8.0
78
+
79
+ Upgrading from 7.7.0. Impact: recommended.
80
+
81
+ ### May stop
82
+
83
+ - The SDK now stops accidental text selection, the long-press copy menu and the tap highlight across your game's document, so a game can drop its own rules for that. Your own style rules always win, and inputs, text areas and editable content stay typeable and selectable. Text a player could select and copy before no longer is, unless your own styles make it selectable. Pass selectableText as true to createGem only if your game is mostly text a player should be able to copy.
84
+
85
+ ### New capability
86
+
87
+ - A room says whether it is self-hosted in selfHosted: its dedicated server runs on the machine of the player who created it, as it does in the Gem editor. The SDK asks for such a room on your behalf where the embedder hosts locally, and there is no option for a game to set. It is false on every room a player in the Arcade is in.
88
+
89
+ ## 7.7.0
90
+
91
+ Upgrading from 7.6.0. Impact: required.
92
+
93
+ ### Must change
94
+
95
+ - In the server API types, occurred_at on a leaderboard row and on an entity leaderboard row is now optional and may be null. Code that assumes it is always a number must handle it missing. LeaderboardResponse and EntityLeaderboardResponse are now written as the shared LeaderboardPeriodFields joined with their own fields, so every field they had is still there.
96
+
97
+ ### Rebuild
98
+
99
+ - Server API unions gained values. OrderSource has earning_reset, and InventoryChange has a new earning_limits_reset change. A switch that assumes it knows every OrderSource value or every InventoryChange type must handle the new one. Other code that compiles today keeps compiling.
100
+ - The server API reads getLeaderboard and getEntityLeaderboard accept three more options, periods_back, period and player_id, and the option types GetLeaderboardOptions and EntityLeaderboardOptions now extend the new PeriodOptions. Code that compiles today keeps compiling.
101
+
102
+ ### New capability
103
+
104
+ - gem.leaderboards can read a board scoped to one period of a challenge, such as today's board or this week's. Every read takes periodsBack, where 0 is the current period and 1 is the one before, or period, the id of a period from an earlier result. Name at most one of them. A result from such a board carries period, which says which period it is, when it opens and closes, and whether it is final. A serverless function or a dedicated server passes playerId so the board answers for that player's own day. On a board that is not scoped to periods, both selectors are refused rather than ignored.
105
+
106
+ ## 7.6.0
107
+
108
+ Upgrading from 7.5.0. Impact: required.
109
+
110
+ ### Must change
111
+
112
+ - createGem now throws GemUnframedError when the page is opened on its own instead of inside a frame. Before, such a game constructed and then waited for a bridge that never came, with no player, no saves and no config. Run your game where it is framed and handed an identity: the Arcade, the Gem editor or the Gem dev host. A game that is only ever played there changes nothing. When the framing page is not one the game allows, the refusal is now final: awaitBridge, awaitIdentity, awaitPlayer and awaitCohorts answer at once instead of timing out.
113
+
114
+ ### New capability
115
+
116
+ - A serverless function written in JavaScript can use the SDK through a new function entry of the package. createGem there returns the same platform-backed calls a browser game makes, bound to the player who called the function. Nothing is retried, a conditional save is refused before it is sent, and gem.functions and gem.gameConfig throw because a function has neither. This entry is in the package only.
117
+
19
118
  ## 7.5.0
20
119
 
21
120
  Upgrading from 7.4.2. Impact: required.
package/docs/friends.md CHANGED
@@ -11,9 +11,10 @@ for (const friend of page.friends) {
11
11
  }
12
12
  ```
13
13
 
14
- That read, and one ask — [sending a friend a challenge](#friend-challenges), which the Arcade
15
- confirms with the player and sends itself — are the whole surface. The boundary around them is the
16
- thing to understand before you design against it.
14
+ That read, one ask — [sending a friend a challenge](#friend-challenges), which the Arcade
15
+ confirms with the player and sends itself — and [reading the challenges the player
16
+ received](#reading-the-challenges-a-player-received) in your game are the whole surface. The
17
+ boundary around them is the thing to understand before you design against it.
17
18
 
18
19
  ## What the read is, and is not
19
20
 
@@ -31,7 +32,8 @@ Everything else about the friend graph stays on the Arcade, by design:
31
32
  platform screens with the player's own account credential — a game frame holds a play token,
32
33
  and a play token cannot touch the graph. Sending a [friend challenge](#friend-challenges) does
33
34
  not touch it either: your game asks, and the Arcade sends only after the player confirms in its
34
- own sheet.
35
+ own sheet. The one write a play token has is marking one of the player's own friend challenges
36
+ answered (`gem.friends.markAnswered`), which records that they played it and tells nobody.
35
37
  - **No states.** Pending requests, blocks and relationship history do not appear. An accepted
36
38
  friend appears; everyone else does not exist on this surface.
37
39
  - **No presence, no invites.** Nothing here says who is online or delivers an invite — an invite
@@ -174,15 +176,88 @@ existing `instanceof GemApiError` handling keeps working:
174
176
 
175
177
  ### When a challenge launches your game
176
178
 
177
- The friend's tap opens your game with the challenge's params in its launch query, read exactly as
178
- [your own launch keys](getting-started.md#linking-to-your-game) are: the param keys are
179
- launch-query keys, and every value arrived in a link, so validate it the same way. The Arcade adds
180
- one key of its own, `gemChallenge`, which is the platform's, not yours: never parse it. The SDK
181
- method that reads it ships in a later release.
179
+ The friend's tap opens your game with the challenge's params in its launch query, and with one key
180
+ of the platform's own, `gemChallenge`. **Do not read either from the URL to find the challenge.**
181
+ Ask the SDK:
182
+
183
+ ```js
184
+ const challenge = await gem.friends.launchedChallenge();
185
+ if (challenge?.template === 'beat_code_jar') {
186
+ // Check every param before it does anything, as you would a launch key: the template
187
+ // constrained it when it was sent, but your config may have changed since.
188
+ const jar = typeof challenge.params.jar === 'string' && /^[A-Z0-9]{7}$/.test(challenge.params.jar)
189
+ ? challenge.params.jar
190
+ : null;
191
+ if (jar) {
192
+ startJar(jar, {
193
+ from: challenge.from, // who sent it, from the platform's records
194
+ replay: challenge.answeredAt !== undefined, // already played once
195
+ });
196
+ }
197
+ }
198
+ ```
199
+
200
+ `launchedChallenge()` reads `gemChallenge` once, when `createGem` runs — so cleaning your URL
201
+ with `history.replaceState` afterwards changes nothing — and looks the challenge up on the
202
+ platform. The order matters the other way too: call `createGem` before you clean the URL, or
203
+ leave `gemChallenge` in place, because a key removed before `createGem` runs is never seen and
204
+ `launchedChallenge()` resolves `null` with no error. You get the sender as a [friend row](#what-the-read-is-and-is-not), the template key and
205
+ the params exactly as they were sent.
206
+
207
+ It **resolves `null`**, never rejects, for every launch that is not this player's challenge in
208
+ this game: no `gemChallenge` key, a value that is not a challenge id, a link naming someone
209
+ else's challenge, one from another game or channel, and every guest launch. Treat `null` as an
210
+ ordinary launch. Only a real failure — the platform unreachable, a context with no player —
211
+ rejects.
212
+
213
+ **Never put a name in a param, and never read one out.** A param key like `by=`, `from=` or
214
+ `name=` is forbidden: the sender is `challenge.from`, resolved by the platform, and a name in a
215
+ param is one nobody checked — your game would render whatever the sending client put there.
182
216
 
183
217
  **A relaunch replays the same challenge.** When the player presses Play again in the Arcade, it
184
- reuses the launch query, so the same params arrive again. Make answering a challenge idempotent:
185
- a second run of the same challenge is a replay, not a new one.
218
+ reuses the launch query, so the same challenge comes back. Tell a replay from a first attempt by
219
+ `answeredAt`, and make answering idempotent.
220
+
221
+ ### Marking a challenge answered
222
+
223
+ When the attempt the challenge asked for is over — won or lost — mark it:
224
+
225
+ ```js
226
+ await gem.friends.markAnswered(challenge.id);
227
+ ```
228
+
229
+ It is idempotent: a challenge keeps the first time it was answered, and a second call changes
230
+ nothing. It tells the sender nothing — there are no read receipts — so it is your game's own
231
+ record, read back as `answeredAt` on every later read. It rejects `not_found` for an id that is
232
+ not one of this player's challenges in this game and channel (one answer for unknown and
233
+ someone else's), and `invalid`, before anything is sent, for a value that cannot be a challenge
234
+ id at all.
235
+
236
+ ### Reading the challenges a player received
237
+
238
+ `gem.friends.inbox()` lists the friend challenges this player holds **in this game, on this
239
+ channel**, in every state:
240
+
241
+ ```js
242
+ let page = await gem.friends.inbox();
243
+ renderChallenges(page.challenges);
244
+ while (page.nextCursor) {
245
+ page = await gem.friends.inbox({ cursor: page.nextCursor });
246
+ renderChallenges(page.challenges);
247
+ }
248
+ ```
249
+
250
+ - **Answered challenges are included, and sort last**; within each part, newest first. Filter on
251
+ `answeredAt` if you only want the open ones.
252
+ - **A guest's page is empty**, not a refusal — a guest is never sent a challenge. Build the screen
253
+ for the empty state first.
254
+ - **Paged like the friends list**: default 50, at most 200, an opaque cursor passed back verbatim.
255
+ A player holds at most 200 friend challenges across every game, so one max-size page is all of
256
+ them.
257
+ - `openedAt` is when the player pressed Play on it in the Arcade. There is no "seen" time: that is
258
+ the Arcade's, not your game's.
259
+
260
+ For the challenge this launch carried, use `launchedChallenge()` rather than searching the inbox.
186
261
 
187
262
  ## Inspecting a player: game profiles
188
263
 
@@ -50,7 +50,7 @@ either way.
50
50
  ```jsonc
51
51
  // package.json
52
52
  "dependencies": {
53
- "@indiegems/gem-web-sdk": "7.9.0"
53
+ "@indiegems/gem-web-sdk": "8.0.0"
54
54
  }
55
55
  ```
56
56
 
@@ -342,8 +342,11 @@ That is the whole contract for your own keys. The bridge carries no launch messa
342
342
  has no API for them: the arcade does the forwarding, so a build you published on any SDK version
343
343
  gets it without a code change. The one key that is not yours is the platform's `gemChallenge`
344
344
  (below), which belongs to [friend challenges](friends.md#friend-challenges): never define it, and
345
- never parse it yourself. The SDK will read it for you, and that is the only launch key it will
346
- ever read: the method ships in a later release, and until then a game has nothing to do with it.
345
+ never parse it yourself. The SDK reads it for you — `gem.friends.launchedChallenge()` answers the
346
+ challenge it names, or `null` — and that is the only launch key it will ever read. It is read once,
347
+ when `createGem` runs, so cleaning your URL afterwards does not lose it. Call `createGem` before
348
+ you clean the URL, or leave `gemChallenge` in place: a key removed before `createGem` runs is never
349
+ seen, and `launchedChallenge()` then resolves `null` with no error.
347
350
 
348
351
  What reaches the frame:
349
352
 
@@ -365,9 +368,10 @@ What reaches the frame:
365
368
  letter, so the arcade keeps that space for itself.
366
369
 
367
370
  - **One platform key.** `gemChallenge` does reach the frame — the arcade adds it when a player
368
- opens a friend challenge — but it is the platform's, not yours. Do not define a key of that
369
- name, and treat every key beginning with `gem` as reserved: a friend-challenge template refuses
370
- a param named that way.
371
+ opens a friend challenge — but it is the platform's, not yours: read the challenge with
372
+ `gem.friends.launchedChallenge()`, never from the URL. Do not define a key of that name, and
373
+ treat every key beginning with `gem` as reserved: a friend-challenge template refuses a param
374
+ named that way.
371
375
  - **Bounded.** At most 16 parameters. A key is a letter followed by up to 63 letters, digits,
372
376
  `_` or `-`. A value is at most 512 characters with no control or format characters. An entry
373
377
  that breaks a rule is dropped on its own; the rest of the link still arrives.
@@ -33,8 +33,8 @@
33
33
  },
34
34
  {
35
35
  "file": "changelog.md",
36
- "sha256": "3398bf6f55c503520167dc9cfbee06da7f0552125a4b2050d7e508096c8e256f",
37
- "updated": "2026-10-02T21:36:48Z"
36
+ "sha256": "37bae4170e89587ee8c498891fa2dac008f09fb01fcfbcf87b1310340d2cfc62",
37
+ "updated": "2026-10-06T19:43:21Z"
38
38
  },
39
39
  {
40
40
  "file": "cross-promotion.md",
@@ -58,8 +58,8 @@
58
58
  },
59
59
  {
60
60
  "file": "friends.md",
61
- "sha256": "1c0a427a4e86d4c0e48faad9a80bdef9d528a60485b5aadee18d7759d9051e63",
62
- "updated": "2026-10-01T16:38:53Z"
61
+ "sha256": "002e6f61050cd188baa0b9b056c9ceaccd96e653361e0c28646baa5632e525df",
62
+ "updated": "2026-10-06T16:37:15Z"
63
63
  },
64
64
  {
65
65
  "file": "game-economy-tutorial.md",
@@ -68,8 +68,8 @@
68
68
  },
69
69
  {
70
70
  "file": "getting-started.md",
71
- "sha256": "63a367fdcf79abee6a7978057cb1b7065f0365cd6fa5a895cbd2b18f76d29701",
72
- "updated": "2026-10-03T22:38:04Z"
71
+ "sha256": "77ce7b2223d18267ac3ca2f49e09481752539f2674910e2cb12832825c945000",
72
+ "updated": "2026-10-06T19:43:21Z"
73
73
  },
74
74
  {
75
75
  "file": "inventory-and-catalogs.md",
@@ -113,33 +113,38 @@
113
113
  },
114
114
  {
115
115
  "file": "saving-player-progress.md",
116
- "sha256": "e556b9ef033b0ae611a7d136da3422c8145367939848eda193ab2674087c884f",
117
- "updated": "2026-10-01T18:37:21Z"
116
+ "sha256": "08778cccc7d7a9a26a005a070bb2cc98784752ebec9dfce9873ef3d4e674e1e0",
117
+ "updated": "2026-10-05T16:53:25Z"
118
118
  },
119
119
  {
120
120
  "file": "server-bundles.md",
121
- "sha256": "419a1587827f494f638f331571a6385f4de080efb062e83656a9d2c50ee3473b",
122
- "updated": "2026-10-03T22:38:04Z"
121
+ "sha256": "49f51b37e969b6120d2df6cf6d2f33d2856c1602fd7504a7b308a9ea21c9b502",
122
+ "updated": "2026-10-06T19:43:21Z"
123
123
  },
124
124
  {
125
125
  "file": "serverless-functions-js.md",
126
- "sha256": "86b0f9f0b742a662ebb78b1538cbbc399309725faf22f46f613b63b5e0864082",
127
- "updated": "2026-10-02T17:17:29Z"
126
+ "sha256": "8d5bda66dfebd93e80fe97b2f09c0651c26dc83b16827bdae7aa25f198f89e0b",
127
+ "updated": "2026-10-03T00:26:43Z"
128
128
  },
129
129
  {
130
130
  "file": "serverless-functions.md",
131
- "sha256": "c830daf736713744909700bf93ff5ba139b8b5d8ad3c2b4e057ca813fc6179f7",
132
- "updated": "2026-10-02T18:52:17Z"
131
+ "sha256": "1edaa60a8aaf20499f24d661f169790cacfdb11c26529735a93e5b7b996e5432",
132
+ "updated": "2026-10-03T00:26:43Z"
133
133
  },
134
134
  {
135
135
  "file": "trading-and-gifting.md",
136
136
  "sha256": "d7ee3c85540ab8aae29988c16a76bee3ba03cc66b10d2faaf5cd7210a80a0544",
137
137
  "updated": "2026-08-26T03:27:49Z"
138
138
  },
139
+ {
140
+ "file": "vibration.md",
141
+ "sha256": "3608da64abcef207cf0875f9d32f3aed88d4fb23512bc70d0a9684a570263a44",
142
+ "updated": "2026-10-06T17:36:25Z"
143
+ },
139
144
  {
140
145
  "file": "what-a-game-must-do.md",
141
- "sha256": "e5d0e2622077d4e41d1f1786c834e6280356f525146193285d062c4209ac33bd",
142
- "updated": "2026-10-03T22:11:20Z"
146
+ "sha256": "436a8b61c46a3de6c7d5db65293bdcd17426412742dffd21f976bf87d1418d88",
147
+ "updated": "2026-10-06T17:36:25Z"
143
148
  }
144
149
  ]
145
150
  }
@@ -436,6 +436,47 @@ game, and a key you experiment with on `dev` cannot break a shipped build.
436
436
  | a difficulty, a control scheme, a level counter | `gem.me.settings` — a string per key | yes |
437
437
  | a volume, a quality preset, a dismissed tip | `gem.deviceSettings` — one object per device | **no** |
438
438
 
439
+ ## Starting over as a new player while you test
440
+
441
+ Testing a first launch, a tutorial or a new player's economy means getting your own account
442
+ back to nothing on your game. Do not write code for it. A throwaway serverless function, or a
443
+ reset button you mean to remove later, is the workaround this replaces. gemctl does it, on
444
+ `dev` or `test`, for the account you are signed in as:
445
+
446
+ ```bash
447
+ gemctl support reset --player me --channel dev --reason "fresh tutorial run"
448
+ ```
449
+
450
+ `--channel test` works the same way. `--reason` is required and is kept on the audit record, so
451
+ say why in a few words, with no personal data or tokens. Run it from your project's directory,
452
+ or add `--game <game-id>`.
453
+
454
+ The order matters, and two of the steps are the ones people skip:
455
+
456
+ 1. **Close the game first.** A running game still holds the old state, and its next autosave
457
+ writes it straight back over the reset.
458
+ 2. **Run the reset.**
459
+ 3. **Clear the site data of the page the game runs in, with the game still closed.** That is
460
+ the Arcade, or the page `gemctl serve` opened, not the game's own origin. `gem.deviceSettings`
461
+ lives in that page's storage on this device, and no reset reaches it. Skip this and the old
462
+ volume, quality and dismissed tips come back. Clearing the Arcade's site data also signs you
463
+ out of it and clears the device settings of every game you have played there on this device;
464
+ the page `gemctl serve` opened holds only the game you are serving.
465
+ 4. **Reopen the game.** It should see what a new player sees: `null` from
466
+ `gem.me.saves.get`, an empty `gem.me.settings.list()`, and `null` device settings.
467
+
468
+ The reset clears this player's state **in this game, on that one channel**: saves,
469
+ `gem.me.settings`, inventory and progressions, and the rest of what the game keeps for them.
470
+ It does not touch your other channels, your other games, achievements, or your Gems and
471
+ Tickets. Your purchases and what you paid stay on record, but an item a purchase delivered on
472
+ that channel is cleared with the rest, and the answer lists those purchases.
473
+ `gemctl support reset --help` has the full list, and the answer names whatever it left alone.
474
+
475
+ **It never runs on `release`.** gemctl refuses before sending anything, and so does the
476
+ platform. It is a tool for testing your own game, not something a game ships. A "New game" or
477
+ "Reset progress" button for players is game code: it overwrites or clears the player's slots
478
+ with `gem.me.saves` and their keys with `gem.me.settings`, as described above.
479
+
439
480
  ## What the platform already does with these, so you do not
440
481
 
441
482
  **A player's erasure covers their saves.** When an account is erased the stored payloads go with
@@ -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/v7.9.0/gem-web-sdk.tgz
84
- curl -fsSL https://downloads.gemarcade.com/sdk/v7.9.0/gem-web-sdk.tgz.sri
83
+ curl -fsSL -O https://downloads.gemarcade.com/sdk/v8.0.0/gem-web-sdk.tgz
84
+ curl -fsSL https://downloads.gemarcade.com/sdk/v8.0.0/gem-web-sdk.tgz.sri
85
85
  openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
86
86
  ```
87
87
 
@@ -33,8 +33,9 @@ message and the stack go to the platform's log for the invocation, never to the
33
33
  `handleJson` whose input is not JSON, or whose return value JSON cannot carry, fails the same way.
34
34
 
35
35
  **`console.*` reaches the invocation's log** at the matching level (`debug`, `info`, `warn`,
36
- `error`); `console.log` and every other method log at `info`. As with a component's `log`, nothing
37
- reads it back to you: the invocation id is what you quote about one call.
36
+ `error`); `console.log` and every other method log at `info`. As with a component's `log`,
37
+ `gemctl function logs` reads it back for a call on `dev`, and a call on `test` or `release` keeps
38
+ no line.
38
39
 
39
40
  ## 2. Writing one
40
41
 
@@ -81,7 +81,7 @@ everything a function can reach:
81
81
  | `call-api` | One request to the platform API, made **as your game's server**: a method, a `/v1/` path and a JSON body, and nothing else |
82
82
  | `download-link` | A save's contents, fetched from the download link the save's record carries |
83
83
  | `upload-link` | A save's new contents, sent to the upload link the platform issued for them |
84
- | `log` | A line kept on the platform under this call's invocation id, cut at 1 KiB |
84
+ | `log` | A line kept 24 hours under this call's invocation id, on `dev` only, cut at 1 KiB |
85
85
  | `random-bytes` | Randomness. The function has no other source |
86
86
  | `now-ms` | Unix time in milliseconds. The function has no other clock |
87
87
 
@@ -120,9 +120,9 @@ Anything that changes several players at once — settling a match, trading betw
120
120
  granting a whole room its rewards — is your **game server's** job, or the job of a function your
121
121
  server calls, which is not bound to a player.
122
122
 
123
- **`log` is not a debugger.** What it writes stays on the platform under the call's invocation id,
124
- and no command reads it back to you. The invocation id is what you quote when you ask the platform
125
- team about one call.
123
+ **`gemctl function logs` reads back what `log` wrote** (section 7), with a line saying how each
124
+ call ended — but only for a call on `dev`, and only for 24 hours. A call on `test` or `release`
125
+ keeps no line, so to see what a tester or a player hit, make the same call on `dev`.
126
126
 
127
127
  **A function cannot call a function.** `call-api` to `/v1/functions/invoke` is refused with
128
128
  `function_cannot_invoke`.
@@ -732,8 +732,10 @@ It reports:
732
732
  - `function_message`: your `Err` text, on a `function_error`;
733
733
  - `reason`: on a `function_failed`, which limit or fault ended the call;
734
734
  - `round_trip_ms`: measured by gemctl, from sending the call to the last byte back;
735
- - `invocation_id`: what the platform files the call under. **Keep it.** It is the only handle on
736
- the platform's side of one call, including whatever the function passed to `log`.
735
+ - `invocation_id`: what the platform files the call under. **Keep it.** It is what you pass to
736
+ `--invocation-id` to read that call's lines, and what you quote when you report the call;
737
+ - `logs`: on a `test` or `release` call that reached your function, that it left no log lines. A
738
+ `dev` call that reached it prints the `gemctl function logs` command that reads its lines instead.
737
739
 
738
740
  Anything but `returned` exits 1.
739
741
 
@@ -744,6 +746,48 @@ If the call is refused before anything is invoked, the problem is your sign-in:
744
746
  be a developer on the game, `test` needs a grant on it, and `release` needs a game your account may
745
747
  play.
746
748
 
749
+ ### Reading what it logged
750
+
751
+ `gemctl function logs` reads what your function wrote with `log`, newest first, with one closing
752
+ line per call saying how it ended.
753
+
754
+ ```bash
755
+ gemctl function logs --invocation-id <invocation-id> # one call: what it logged, how it ended
756
+ gemctl function logs --outcome error # every call whose handle returned Err
757
+ gemctl function logs --level warn # what your function warned about
758
+ ```
759
+
760
+ ⚠️ **Only a call on `dev` leaves lines.** A call on `test` or `release` leaves none, so there is
761
+ no channel to choose and nothing here shows what a tester or a player hit. Make the same call on
762
+ `dev`, with the input that failed, and read that.
763
+
764
+ Each row is one of two kinds, and both carry the call's `invocation_id` and its `caller`, `player`
765
+ or `game_server`:
766
+
767
+ - **A line your function logged**: its `level` and its `message`.
768
+ - **The closing line**: `outcome` is `ok`, `error`, `timeout`, `cpu_exceeded` or `failed`, with the
769
+ call's `wall_ms` and `cpu_ms`. On `error`, `message` is your `Err` text. On `failed`, `reason`
770
+ says what ended the call, and when your own code trapped, `message` is the trap's message.
771
+
772
+ Newest first means a call's closing line comes before what it logged. **A call's logging is
773
+ capped**: once it reaches its per-call log limit, that `log` call and every later one go
774
+ unrecorded, and the closing line's `dropped_log_lines` says how many. A log that stops part-way
775
+ with a nonzero count means your function logged too much, not that it went quiet.
776
+
777
+ `--invocation-id`, `--level` (`debug`, `info`, `warn` or `error`) and `--outcome` narrow the page.
778
+ `--level` picks what your function logged and `--outcome` picks closing lines, so the two cannot be
779
+ combined: to see everything a failed call logged, pass its invocation id. The window is the last 24
780
+ hours unless `--start-ns` and `--end-ns` (Unix nanoseconds) narrow it, and a full page ends with a
781
+ hint carrying `--cursor` for the next, older one. There is no follow mode.
782
+
783
+ **An empty page does not prove your function was not called.** Lines take a few seconds to arrive
784
+ and are kept for 24 hours. A call the platform refused before your function ran leaves none — a bad
785
+ credential, a malformed request, a channel serving nothing, a full pool, a component that could not
786
+ be downloaded — and its `invoke` answer is its only record.
787
+
788
+ Every `message` was written by your function's code, and any member of your game's team can read
789
+ it. Log ids, not anything personal about a player.
790
+
747
791
  ## 8. When a call is refused
748
792
 
749
793
  Every answer from a call that reached your function carries an invocation id: success,
@@ -766,7 +810,9 @@ the last column: it is `true` exactly where this table says to retry.
766
810
  | `403 forbidden` | The call carried an account sign-in rather than a game-scoped or game-server token | Call from your game or its server, or through `gemctl function invoke` |
767
811
 
768
812
  A `trap` is a panic, an `unreachable`, an out-of-bounds access or a stack overflow, and no stack
769
- trace comes back with it. The other three reasons are the limits in section 2.
813
+ trace comes back with it. On `dev`, the call's closing line in `gemctl function logs` carries the
814
+ trap's message when your own code trapped (section 7). The other three reasons are the limits in
815
+ section 2.
770
816
 
771
817
  Publishing has its own refusals, and nothing is stored when one happens:
772
818