@indiegems/gem-web-sdk 7.3.1 → 7.4.1

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 (42) hide show
  1. package/README.md +54 -0
  2. package/dist/game.d.ts +156 -22
  3. package/dist/gem-web-sdk.bridge.js +1 -1
  4. package/dist/gem-web-sdk.esm.js +2 -2
  5. package/dist/gem-web-sdk.game.js +2 -2
  6. package/dist/gem-web-sdk.min.js +2 -2
  7. package/dist/gem-web-sdk.sdk.js +2 -2
  8. package/dist/gem-web-sdk.server.js +2 -2
  9. package/dist/index.d.ts +89 -6
  10. package/dist/sdk.d.ts +156 -22
  11. package/docs/achievements.md +232 -0
  12. package/docs/analytics.md +271 -0
  13. package/docs/api/api-reference-game.md +15308 -0
  14. package/docs/api/api-reference-server.md +4397 -0
  15. package/docs/api/reference.json +24130 -0
  16. package/docs/arcade-economy.md +263 -0
  17. package/docs/async-opponents.md +396 -0
  18. package/docs/brand-art.md +334 -0
  19. package/docs/cross-promotion.md +137 -0
  20. package/docs/daily-challenges.md +530 -0
  21. package/docs/dedicated-server-flow.md +879 -0
  22. package/docs/deprecation-policy.md +124 -0
  23. package/docs/friends.md +233 -0
  24. package/docs/game-economy-tutorial.md +326 -0
  25. package/docs/getting-started.md +552 -0
  26. package/docs/inventory-and-catalogs.md +1035 -0
  27. package/docs/leaderboards.md +277 -0
  28. package/docs/manifest.json +135 -0
  29. package/docs/match-session.md +288 -0
  30. package/docs/p2p-and-parties.md +783 -0
  31. package/docs/platform-rules.md +372 -0
  32. package/docs/player-sign-in.md +89 -0
  33. package/docs/room-shapes.md +195 -0
  34. package/docs/rooms-and-matches.md +779 -0
  35. package/docs/saving-player-progress.md +457 -0
  36. package/docs/server-bundles.md +1041 -0
  37. package/docs/serverless-functions.md +748 -0
  38. package/docs/trading-and-gifting.md +98 -0
  39. package/docs/what-a-game-must-do.md +1231 -0
  40. package/package.json +7 -2
  41. package/schema/bridge.v1.json +440 -7
  42. package/schema/capabilities.public.json +189 -0
@@ -0,0 +1,4397 @@
1
+ # The server surface — API reference
2
+
3
+ Generated from `@indiegems/gem-web-sdk/sdk` v7.4.1. Every symbol below links to its page on the Gem Arcade developer site.
4
+
5
+ `@indiegems/gem-web-sdk/sdk` is the one import a game writes, wherever its code runs. `@indiegems/gem-web-sdk/server` exports exactly the symbols on this page and nothing else, for a build that wants only this half.
6
+
7
+ ```js
8
+ import { defineServer } from '@indiegems/gem-web-sdk/sdk';
9
+ ```
10
+
11
+ One dependency covers both halves of a game, and it is written out in [getting-started](https://gemarcade.com/developer/docs/getting-started#3-add-the-sdk) — the only place it is written out, so there is one copy to keep current. It is a public download needing no account and no credential, it goes in `dependencies`, and nothing is built on install. If you followed that page, the server entry is already installed.
12
+
13
+ ## AnalyticsValue
14
+
15
+ [AnalyticsValue](https://gemarcade.com/developer/api/server#analyticsvalue)
16
+
17
+ ```ts
18
+ type AnalyticsValue = string | number
19
+ ```
20
+
21
+ A value attached to an event.
22
+
23
+ A **string** is a dimension and a **number** is a measure, and which one a
24
+ key is was decided in your game config, not here. Dimensions are closed sets
25
+ declared ahead of time — that is the whole reason there is no free-text
26
+ property — and measures are the numbers the platform sums, averages and
27
+ takes the min and max of.
28
+
29
+ ## AnalyticsRejection
30
+
31
+ [AnalyticsRejection](https://gemarcade.com/developer/api/server#analyticsrejection)
32
+
33
+ ```ts
34
+ interface AnalyticsRejection
35
+ ```
36
+
37
+ One event the platform refused, inside an otherwise fine batch.
38
+
39
+ ## AnalyticsRejection.index
40
+
41
+ [AnalyticsRejection.index](https://gemarcade.com/developer/api/server#analyticsrejection-index)
42
+
43
+ ```ts
44
+ readonly index: number
45
+ ```
46
+
47
+ Which event was refused, as an index into the batch that was sent.
48
+
49
+ ⚠️ NOT an index into anything you hold. The SDK batches on its own
50
+ schedule, so this is only meaningful beside the code — which is the part
51
+ worth acting on.
52
+
53
+ ## AnalyticsRejection.code
54
+
55
+ [AnalyticsRejection.code](https://gemarcade.com/developer/api/server#analyticsrejection-code)
56
+
57
+ ```ts
58
+ readonly code: string
59
+ ```
60
+
61
+ A stable snake_case code. Read the code, never the prose.
62
+
63
+ `analytics_not_declared` (this channel resolves no `analytics_events`
64
+ section at all — promote a config that has one), `unknown_event` (the
65
+ channel declares a set and this name is not in it), `unknown_dimension`,
66
+ `missing_dimension`, `dimension_value_not_declared`, `unknown_measure`,
67
+ `measure_wrong_type`, `source_not_permitted`.
68
+
69
+ ## AnalyticsFlushResult
70
+
71
+ [AnalyticsFlushResult](https://gemarcade.com/developer/api/server#analyticsflushresult)
72
+
73
+ ```ts
74
+ interface AnalyticsFlushResult
75
+ ```
76
+
77
+ What `AnalyticsApi.flush` resolves with.
78
+
79
+ ## AnalyticsFlushResult.accepted
80
+
81
+ [AnalyticsFlushResult.accepted](https://gemarcade.com/developer/api/server#analyticsflushresult-accepted)
82
+
83
+ ```ts
84
+ readonly accepted: number
85
+ ```
86
+
87
+ How many events the platform accepted and wrote.
88
+
89
+ ## AnalyticsFlushResult.rejected
90
+
91
+ [AnalyticsFlushResult.rejected](https://gemarcade.com/developer/api/server#analyticsflushresult-rejected)
92
+
93
+ ```ts
94
+ readonly rejected: readonly AnalyticsRejection[]
95
+ ```
96
+
97
+ Every event the platform refused, by index into the batch that was sent.
98
+
99
+ ⚠️ REJECTIONS ARRIVE INSIDE A 200, never as a thrown error, and the
100
+ accepted events in the same batch were still written. An empty array is
101
+ the ordinary case.
102
+
103
+ ## AnalyticsFlushResult.dropped
104
+
105
+ [AnalyticsFlushResult.dropped](https://gemarcade.com/developer/api/server#analyticsflushresult-dropped)
106
+
107
+ ```ts
108
+ readonly dropped: number
109
+ ```
110
+
111
+ How many events the local queue threw away since the last successful
112
+ flush, because it was full. The oldest go first.
113
+
114
+ A number worth alarming on: it is nonzero only when the game is producing
115
+ events faster than the platform accepts them.
116
+
117
+ ## AnalyticsFlushResult.duplicate
118
+
119
+ [AnalyticsFlushResult.duplicate](https://gemarcade.com/developer/api/server#analyticsflushresult-duplicate)
120
+
121
+ ```ts
122
+ readonly duplicate: boolean
123
+ ```
124
+
125
+ True when the platform had already seen this batch and wrote nothing.
126
+
127
+ A retry that the first attempt had in fact delivered. Not an error — it is
128
+ what stops a flush racing a `pagehide` from being counted twice — and
129
+ `accepted` is 0 on one.
130
+
131
+ ## Base64String
132
+
133
+ [Base64String](https://gemarcade.com/developer/api/server#base64string)
134
+
135
+ ```ts
136
+ type Base64String = string & { __contentEncoding: "base64" }
137
+ ```
138
+
139
+ A base64-encoded value — a `string`, but not one you can substitute an
140
+ ordinary string for.
141
+
142
+ Some platform fields carry BYTES inside a JSON string, encoded as base64. An
143
+ entity setting's `value` is the one you are most likely to meet. Passing
144
+ ordinary text there fails the whole request, and it fails at runtime, on the
145
+ server, after you shipped.
146
+
147
+ This type moves that failure to your build. `{ value: JSON.stringify(state) }`
148
+ type-checks perfectly against `string` and is exactly the mistake being
149
+ stopped.
150
+
151
+ MAKE ONE WITH [toBase64](https://gemarcade.com/developer/api/server#tobase64). Writing `as Base64String` instead silences the
152
+ error and puts the trap back.
153
+
154
+ ## toBase64
155
+
156
+ [toBase64](https://gemarcade.com/developer/api/server#tobase64)
157
+
158
+ ```ts
159
+ function toBase64(value: string | Uint8Array<ArrayBufferLike>): Base64String
160
+ ```
161
+
162
+ Encode text or bytes for a field that wants base64.
163
+
164
+ The only way to produce a [Base64String](https://gemarcade.com/developer/api/server#base64string), and the reason not to reach
165
+ for `btoa` yourself: `btoa` takes a *binary* string and throws on any
166
+ character above U+00FF, so `btoa(name)` works all through your testing and
167
+ then fails on the first player whose display name is not Latin-1. This
168
+ encodes a string as UTF-8 first.
169
+
170
+ | Parameter | Type | Description |
171
+ | --- | --- | --- |
172
+ | `value` | `string \| Uint8Array<ArrayBufferLike>` | Text, or bytes you already have. |
173
+
174
+ **Returns.** The encoded value, typed so the field will accept it.
175
+
176
+ ```js
177
+ await gem.api.putEntityGameSetting('loadout', {
178
+ value: toBase64(JSON.stringify(loadout)),
179
+ });
180
+ ```
181
+
182
+ ## GemErrorReason
183
+
184
+ [GemErrorReason](https://gemarcade.com/developer/api/server#gemerrorreason)
185
+
186
+ ```ts
187
+ type GemErrorReason = "invalid" | "forbidden" | "not_found" | "conflict" | "precondition_failed" | "not_modified" | "insufficient_funds" | "too_large" | "rate_limited" | "unavailable"
188
+ ```
189
+
190
+ What went wrong, in terms you can act on.
191
+
192
+ BRANCH ON THIS, not on the HTTP status — one status covers several distinct
193
+ refusals, and the reason is what tells them apart. It is carried by
194
+ [GemApiError.reason](https://gemarcade.com/developer/api/server#gemapierror-reason).
195
+
196
+ ## GemApiError
197
+
198
+ [GemApiError](https://gemarcade.com/developer/api/server#gemapierror)
199
+
200
+ ```ts
201
+ class GemApiError
202
+ ```
203
+
204
+ A refusal from the Gem Arcade backend. Any call that reaches the platform can
205
+ throw one.
206
+
207
+ You catch this; you never construct it. Branch on [GemApiError.reason](https://gemarcade.com/developer/api/server#gemapierror-reason)
208
+ for what went wrong and [GemApiError.retryable](https://gemarcade.com/developer/api/server#gemapierror-retryable) for whether another
209
+ attempt could help — not on [GemApiError.status](https://gemarcade.com/developer/api/server#gemapierror-status), because one status
210
+ covers several distinct refusals.
211
+
212
+ ```js
213
+ try {
214
+ await somePlatformCall();
215
+ } catch (err) {
216
+ if (err instanceof GemApiError && err.retryable) {
217
+ const wait = err.retryAfterSeconds ?? 1;
218
+ // try again after `wait` seconds
219
+ } else {
220
+ throw err;
221
+ }
222
+ }
223
+ ```
224
+
225
+ ## GemApiError.reason
226
+
227
+ [GemApiError.reason](https://gemarcade.com/developer/api/server#gemapierror-reason)
228
+
229
+ ```ts
230
+ readonly reason: GemErrorReason
231
+ ```
232
+
233
+ What went wrong, in terms you can act on — `invalid`, `forbidden`,
234
+ `not_found`, `conflict`, `precondition_failed`, `not_modified`,
235
+ `insufficient_funds`, `too_large`, `rate_limited` or `unavailable`.
236
+
237
+ This is the field to branch on. Two refusals can share a status and mean
238
+ different things; they do not share a reason.
239
+
240
+ ## GemApiError.status
241
+
242
+ [GemApiError.status](https://gemarcade.com/developer/api/server#gemapierror-status)
243
+
244
+ ```ts
245
+ readonly status: number
246
+ ```
247
+
248
+ The HTTP status the server answered with.
249
+
250
+ Carried for logs and bug reports. Prefer [GemApiError.reason](https://gemarcade.com/developer/api/server#gemapierror-reason) for
251
+ decisions: several distinct refusals map onto one status, so branching
252
+ here treats different problems as the same one.
253
+
254
+ ## GemApiError.detail
255
+
256
+ [GemApiError.detail](https://gemarcade.com/developer/api/server#gemapierror-detail)
257
+
258
+ ```ts
259
+ readonly detail: string
260
+ ```
261
+
262
+ The server's own sentence, without the `reason (status):` prefix that
263
+ `message` carries.
264
+
265
+ Use this when you want to quote the platform — in a log line, or a bug
266
+ report. It is not written for players, so do not put it on screen.
267
+
268
+ ## GemApiError.retryAfterSeconds
269
+
270
+ [GemApiError.retryAfterSeconds](https://gemarcade.com/developer/api/server#gemapierror-retryafterseconds)
271
+
272
+ ```ts
273
+ readonly retryAfterSeconds?: number
274
+ ```
275
+
276
+ How long the server asked you to wait, in whole seconds.
277
+
278
+ ABSENT MEANS "NO ESTIMATE GIVEN", NEVER "DO NOT RETRY" —
279
+ [GemApiError.retryable](https://gemarcade.com/developer/api/server#gemapierror-retryable) is what answers that. A refusal can be
280
+ worth repeating and still name no wait, so treat absence as "pick your
281
+ own backoff" rather than as a stop signal. The platform estimates a wait
282
+ for capacity and for rate limits, and sends none for the refusals it
283
+ cannot put a number on.
284
+
285
+ ## GemApiError.code
286
+
287
+ [GemApiError.code](https://gemarcade.com/developer/api/server#gemapierror-code)
288
+
289
+ ```ts
290
+ readonly code?: string
291
+ ```
292
+
293
+ The platform's own error code, when it sent one —
294
+ `too_many_concurrent_matches`, `cluster_target_required`,
295
+ `no_compatible_server`.
296
+
297
+ Finer-grained than [GemApiError.reason](https://gemarcade.com/developer/api/server#gemapierror-reason), and the only way to tell
298
+ apart two refusals that share both a reason and a status. Absent on most
299
+ refusals, so treat it as extra information rather than something to
300
+ depend on.
301
+
302
+ ON A PURCHASE IT IS THE FIELD TO BRANCH ON. `gem.me.mutations.purchase`
303
+ refusals carry the platform's purchase codes — `insufficient_funds`,
304
+ `recipe_locked`, `price_mismatch`, `not_purchasable`, … — the same
305
+ strings `gem.purchases.request` answers as its `reason`, so one `switch`
306
+ on `err.code` covers both purchase paths.
307
+
308
+ ## GemApiError.retryable
309
+
310
+ [GemApiError.retryable](https://gemarcade.com/developer/api/server#gemapierror-retryable)
311
+
312
+ ```ts
313
+ get retryable(): boolean
314
+ ```
315
+
316
+ Whether another attempt could plausibly succeed.
317
+
318
+ True for `unavailable` and `rate_limited` and false for everything else —
319
+ a `forbidden` or `not_found` will refuse identically however many times you
320
+ send it. Check this before retrying, and wait
321
+ [GemApiError.retryAfterSeconds](https://gemarcade.com/developer/api/server#gemapierror-retryafterseconds) if the server named one.
322
+
323
+ ## ClientOptions
324
+
325
+ [ClientOptions](https://gemarcade.com/developer/api/server#clientoptions)
326
+
327
+ ```ts
328
+ interface ClientOptions
329
+ ```
330
+
331
+ How a [GemClient](https://gemarcade.com/developer/api/server#gemclient) is built.
332
+
333
+ You rarely construct one yourself — [createGemServer](https://gemarcade.com/developer/api/server#creategemserver) does it — but the
334
+ type has to be nameable to write a helper that takes one.
335
+
336
+ ## ClientOptions.transport
337
+
338
+ [ClientOptions.transport](https://gemarcade.com/developer/api/server#clientoptions-transport)
339
+
340
+ ```ts
341
+ readonly transport: Transport
342
+ ```
343
+
344
+ How requests actually leave. Substitute one to test without a network — see
345
+ [Transport](https://gemarcade.com/developer/api/server#transport).
346
+
347
+ ## ClientOptions.clock
348
+
349
+ [ClientOptions.clock](https://gemarcade.com/developer/api/server#clientoptions-clock)
350
+
351
+ ```ts
352
+ readonly clock: Clock
353
+ ```
354
+
355
+ Where time comes from, for retry backoff. See [Clock](https://gemarcade.com/developer/api/server#clock).
356
+
357
+ ## ClientOptions.maxRetries
358
+
359
+ [ClientOptions.maxRetries](https://gemarcade.com/developer/api/server#clientoptions-maxretries)
360
+
361
+ ```ts
362
+ readonly maxRetries?: number
363
+ ```
364
+
365
+ How many times to retry a read that failed. Defaults to 2 — three attempts
366
+ in all.
367
+
368
+ READS SPEND THIS BUDGET, and so does a write the caller has declared
369
+ replayable with [RequestOptions.replaySafe](https://gemarcade.com/developer/api/server#requestoptions-replaysafe). Every other write gets
370
+ one attempt whatever you set here: a write that timed out may already have
371
+ been applied, and repeating it is how a client charges someone twice.
372
+ Whether a given write is safe to repeat is something only its caller knows,
373
+ so the SDK does not guess.
374
+
375
+ ## RequestOptions
376
+
377
+ [RequestOptions](https://gemarcade.com/developer/api/server#requestoptions)
378
+
379
+ ```ts
380
+ interface RequestOptions
381
+ ```
382
+
383
+ Per-call options, for the occasional call that needs one.
384
+
385
+ ## RequestOptions.keepalive
386
+
387
+ [RequestOptions.keepalive](https://gemarcade.com/developer/api/server#requestoptions-keepalive)
388
+
389
+ ```ts
390
+ readonly keepalive?: boolean
391
+ ```
392
+
393
+ Let the request finish even if the page goes away.
394
+
395
+ For the last write before a player closes the tab. Browsers cap how much
396
+ can be in flight this way, so use it for the write that matters, not for
397
+ every write.
398
+
399
+ ## RequestOptions.ifMatch
400
+
401
+ [RequestOptions.ifMatch](https://gemarcade.com/developer/api/server#requestoptions-ifmatch)
402
+
403
+ ```ts
404
+ readonly ifMatch?: string
405
+ ```
406
+
407
+ Only apply this write if nothing has changed since you read.
408
+
409
+ Pass the version that came back with the read your write is based on. If
410
+ someone else wrote in between, the call fails with `precondition_failed`
411
+ instead of overwriting them — re-read, decide again against the new state,
412
+ and write again.
413
+
414
+ ## RequestOptions.replaySafe
415
+
416
+ [RequestOptions.replaySafe](https://gemarcade.com/developer/api/server#requestoptions-replaysafe)
417
+
418
+ ```ts
419
+ readonly replaySafe?: boolean
420
+ ```
421
+
422
+ Declare that sending this write twice is harmless, so a failure that never
423
+ reached the server can be retried.
424
+
425
+ OFF BY DEFAULT, and it stays off unless you set it. Writes are not retried
426
+ because a write that timed out may already have been applied — but some
427
+ writes commit nothing, and for those the rule costs a caller the request
428
+ with nothing bought. Asking for a signed upload URL is the example: it
429
+ checks and hands back a URL, and asking twice produces a second URL and no
430
+ second effect.
431
+
432
+ SET IT ONLY WHEN REPEATING THE CALL CANNOT CHANGE THE OUTCOME. A purchase,
433
+ a transfer, a grant of currency and anything that appends are all the
434
+ opposite: a retry the caller cannot see is how one purchase becomes two.
435
+ If you are unsure, leave it alone and decide for yourself whether to
436
+ repeat the call.
437
+
438
+ It buys the same budget a read gets — see [ClientOptions.maxRetries](https://gemarcade.com/developer/api/server#clientoptions-maxretries)
439
+ — and nothing else. A refusal the server made deliberately is still
440
+ final: only the failures [GemApiError.retryable](https://gemarcade.com/developer/api/server#gemapierror-retryable) describes, and a
441
+ connection that died with no answer, are attempted again.
442
+
443
+ ## VersionedGetOptions
444
+
445
+ [VersionedGetOptions](https://gemarcade.com/developer/api/server#versionedgetoptions)
446
+
447
+ ```ts
448
+ interface VersionedGetOptions
449
+ ```
450
+
451
+ Options for a read that also wants the value's version back.
452
+
453
+ ## VersionedGetOptions.signal
454
+
455
+ [VersionedGetOptions.signal](https://gemarcade.com/developer/api/server#versionedgetoptions-signal)
456
+
457
+ ```ts
458
+ readonly signal?: AbortSignal
459
+ ```
460
+
461
+ Cancel the read. An aborted call rejects rather than resolving.
462
+
463
+ ## VersionedGetOptions.ifNoneMatch
464
+
465
+ [VersionedGetOptions.ifNoneMatch](https://gemarcade.com/developer/api/server#versionedgetoptions-ifnonematch)
466
+
467
+ ```ts
468
+ readonly ifNoneMatch?: string
469
+ ```
470
+
471
+ Ask "has this changed?" instead of "give me this".
472
+
473
+ Pass the version from your last read. If nothing has changed you get back
474
+ `{ modified: false }` and no body — cheaper for you and for the platform.
475
+
476
+ ## VersionedResponse
477
+
478
+ [VersionedResponse](https://gemarcade.com/developer/api/server#versionedresponse)
479
+
480
+ ```ts
481
+ type VersionedResponse<T> = { modified: true; body: T; etag?: string } | { modified: false; etag?: string }
482
+ ```
483
+
484
+ What a versioned read answers: either the value and its version, or "nothing
485
+ changed".
486
+
487
+ CHECK `modified` FIRST. There is no `body` on the unchanged branch — not an
488
+ empty one, none — so the type will not let you read it until you have asked.
489
+ That is deliberate: an optional `body` would let an unhandled 304 through to
490
+ production as `undefined`.
491
+
492
+ ```js
493
+ const result = await gem.client.getWithEtag(path, { ifNoneMatch: lastEtag });
494
+ if (result.modified) {
495
+ render(result.body);
496
+ lastEtag = result.etag;
497
+ }
498
+ ```
499
+
500
+ ## BytesResponse
501
+
502
+ [BytesResponse](https://gemarcade.com/developer/api/server#bytesresponse)
503
+
504
+ ```ts
505
+ interface BytesResponse
506
+ ```
507
+
508
+ What [GemClient.postBytes](https://gemarcade.com/developer/api/server#gemclient-postbytes) resolves: the answer's raw bytes, and the id
509
+ the platform gave the invocation when there was one.
510
+
511
+ ## BytesResponse.bytes
512
+
513
+ [BytesResponse.bytes](https://gemarcade.com/developer/api/server#bytesresponse-bytes)
514
+
515
+ ```ts
516
+ readonly bytes: Uint8Array
517
+ ```
518
+
519
+ Exactly the bytes that arrived — empty when the answer had no body.
520
+
521
+ ## BytesResponse.invocationId
522
+
523
+ [BytesResponse.invocationId](https://gemarcade.com/developer/api/server#bytesresponse-invocationid)
524
+
525
+ ```ts
526
+ readonly invocationId?: string
527
+ ```
528
+
529
+ The platform's id for this invocation, when the answer named one and the
530
+ runtime could read it. See [TransportResponse.invocationId](https://gemarcade.com/developer/api/server#transportresponse-invocationid).
531
+
532
+ ## GemClient
533
+
534
+ [GemClient](https://gemarcade.com/developer/api/server#gemclient)
535
+
536
+ ```ts
537
+ class GemClient
538
+ ```
539
+
540
+ The HTTP client underneath the generated operations — for calls those do not
541
+ cover.
542
+
543
+ PREFER [ServerApi](https://gemarcade.com/developer/api/server#serverapi). Its methods are typed against the platform's own
544
+ description, so a shape that changes becomes a build error in your bundle.
545
+ This one takes a path and checks nothing you give it.
546
+
547
+ What it does give you is everything around the call: the credential, retries
548
+ on the requests that are safe to retry, and failures already classified into
549
+ [GemApiError](https://gemarcade.com/developer/api/server#gemapierror) so you can branch on a reason instead of a status.
550
+
551
+ READS ARE RETRIED; A WRITE IS NOT, unless its caller declares it replayable
552
+ with [RequestOptions.replaySafe](https://gemarcade.com/developer/api/server#requestoptions-replaysafe). A write that timed out may well have
553
+ been applied, and retrying it silently is how a client duplicates a purchase
554
+ it cannot see — so the default is one attempt, and widening it is a decision
555
+ made one call at a time by whoever knows what the call does.
556
+
557
+ You reach one as `gem.client`; there is normally no reason to construct it.
558
+
559
+ ## GemClient.constructor
560
+
561
+ [GemClient.constructor](https://gemarcade.com/developer/api/server#gemclient-constructor)
562
+
563
+ ```ts
564
+ constructor(options: ClientOptions): GemClient
565
+ ```
566
+
567
+ Build a client. You normally do not — `createGemServer` builds one and hands
568
+ it to you as `gem.client`.
569
+
570
+ | Parameter | Type | Description |
571
+ | --- | --- | --- |
572
+ | `options` | `ClientOptions` | See [ClientOptions](https://gemarcade.com/developer/api/server#clientoptions). |
573
+
574
+ ## GemClient.get
575
+
576
+ [GemClient.get](https://gemarcade.com/developer/api/server#gemclient-get)
577
+
578
+ ```ts
579
+ get<T>(path: string, signal?: AbortSignal): Promise<T>
580
+ ```
581
+
582
+ Read something.
583
+
584
+ | Parameter | Type | Description |
585
+ | --- | --- | --- |
586
+ | `path` | `string` | From the API root, e.g. `/v1/players/me/settings`. |
587
+ | `signal` | `AbortSignal` | Cancels the read. |
588
+
589
+ **Throws.** [GemApiError](https://gemarcade.com/developer/api/server#gemapierror) on a refusal — branch on `reason`.
590
+
591
+ ## GemClient.getWithEtag
592
+
593
+ [GemClient.getWithEtag](https://gemarcade.com/developer/api/server#gemclient-getwithetag)
594
+
595
+ ```ts
596
+ getWithEtag<T>(path: string, options?: VersionedGetOptions): Promise<VersionedResponse<T>>
597
+ ```
598
+
599
+ Read something AND get its version back — or find out it has not changed.
600
+
601
+ The read half of optimistic concurrency: keep the version this returns, pass
602
+ it as `ifMatch` on your write, and the write is refused rather than
603
+ overwriting somebody. Pass it as `ifNoneMatch` here and an unchanged
604
+ resource comes back with no body at all.
605
+
606
+ SEPARATE FROM [GemClient.get](https://gemarcade.com/developer/api/server#gemclient-get) on purpose — `get` answers with the value
607
+ and nothing else, which is what most calls want.
608
+
609
+ | Parameter | Type | Description |
610
+ | --- | --- | --- |
611
+ | `path` | `string` | From the API root. |
612
+ | `options` | `VersionedGetOptions` | See [VersionedGetOptions](https://gemarcade.com/developer/api/server#versionedgetoptions). |
613
+
614
+ **Returns.** See [VersionedResponse](https://gemarcade.com/developer/api/server#versionedresponse) — check `modified` before reading a body.
615
+
616
+ **Throws.** [GemApiError](https://gemarcade.com/developer/api/server#gemapierror) on a refusal.
617
+
618
+ ## GemClient.put
619
+
620
+ [GemClient.put](https://gemarcade.com/developer/api/server#gemclient-put)
621
+
622
+ ```ts
623
+ put<T>(path: string, body: unknown, signal?: AbortSignal, options?: RequestOptions): Promise<T>
624
+ ```
625
+
626
+ Replace something.
627
+
628
+ NOT RETRIED. A write that timed out may already have been applied — set
629
+ [RequestOptions.replaySafe](https://gemarcade.com/developer/api/server#requestoptions-replaysafe) if repeating this one cannot change the
630
+ outcome.
631
+
632
+ | Parameter | Type | Description |
633
+ | --- | --- | --- |
634
+ | `path` | `string` | From the API root. |
635
+ | `body` | `unknown` | What to send. Encoded as JSON. |
636
+ | `signal` | `AbortSignal` | |
637
+ | `options` | `RequestOptions` | See [RequestOptions](https://gemarcade.com/developer/api/server#requestoptions) — `ifMatch` is how you avoid
638
+ overwriting someone else's write. |
639
+
640
+ **Throws.** [GemApiError](https://gemarcade.com/developer/api/server#gemapierror) on a refusal.
641
+
642
+ ## GemClient.post
643
+
644
+ [GemClient.post](https://gemarcade.com/developer/api/server#gemclient-post)
645
+
646
+ ```ts
647
+ post<T>(path: string, body: unknown, signal?: AbortSignal, options?: RequestOptions): Promise<T>
648
+ ```
649
+
650
+ Create something, or perform an action.
651
+
652
+ NOT RETRIED, for the same reason as [GemClient.put](https://gemarcade.com/developer/api/server#gemclient-put), and widened the
653
+ same way.
654
+
655
+ | Parameter | Type | Description |
656
+ | --- | --- | --- |
657
+ | `path` | `string` | From the API root. |
658
+ | `body` | `unknown` | What to send. Encoded as JSON. |
659
+ | `signal` | `AbortSignal` | |
660
+ | `options` | `RequestOptions` | See [RequestOptions](https://gemarcade.com/developer/api/server#requestoptions). |
661
+
662
+ **Throws.** [GemApiError](https://gemarcade.com/developer/api/server#gemapierror) on a refusal.
663
+
664
+ ## GemClient.postBytes
665
+
666
+ [GemClient.postBytes](https://gemarcade.com/developer/api/server#gemclient-postbytes)
667
+
668
+ ```ts
669
+ postBytes(path: string, bytes: Uint8Array, signal?: AbortSignal, options?: RequestOptions): Promise<BytesResponse>
670
+ ```
671
+
672
+ Send raw bytes and get raw bytes back.
673
+
674
+ For a route whose input and output are your own bytes rather than JSON.
675
+ The body goes out as it is, labelled `application/octet-stream`, and a
676
+ success comes back undecoded. A refusal is still a [GemApiError](https://gemarcade.com/developer/api/server#gemapierror).
677
+
678
+ NOT RETRIED, for the same reason as [GemClient.put](https://gemarcade.com/developer/api/server#gemclient-put), and widened the
679
+ same way.
680
+
681
+ | Parameter | Type | Description |
682
+ | --- | --- | --- |
683
+ | `path` | `string` | From the API root. |
684
+ | `bytes` | `Uint8Array` | What to send, byte for byte. |
685
+ | `signal` | `AbortSignal` | |
686
+ | `options` | `RequestOptions` | See [RequestOptions](https://gemarcade.com/developer/api/server#requestoptions). |
687
+
688
+ **Throws.** [GemApiError](https://gemarcade.com/developer/api/server#gemapierror) on a refusal.
689
+
690
+ ## GemClient.patch
691
+
692
+ [GemClient.patch](https://gemarcade.com/developer/api/server#gemclient-patch)
693
+
694
+ ```ts
695
+ patch<T>(path: string, body: unknown, signal?: AbortSignal, options?: RequestOptions): Promise<T>
696
+ ```
697
+
698
+ Change part of something.
699
+
700
+ NOT RETRIED, for the same reason as [GemClient.put](https://gemarcade.com/developer/api/server#gemclient-put), and widened the
701
+ same way.
702
+
703
+ | Parameter | Type | Description |
704
+ | --- | --- | --- |
705
+ | `path` | `string` | From the API root. |
706
+ | `body` | `unknown` | The change. Encoded as JSON. |
707
+ | `signal` | `AbortSignal` | |
708
+ | `options` | `RequestOptions` | See [RequestOptions](https://gemarcade.com/developer/api/server#requestoptions). |
709
+
710
+ **Throws.** [GemApiError](https://gemarcade.com/developer/api/server#gemapierror) on a refusal.
711
+
712
+ ## GemClient.delete
713
+
714
+ [GemClient.delete](https://gemarcade.com/developer/api/server#gemclient-delete)
715
+
716
+ ```ts
717
+ delete<T>(path: string, signal?: AbortSignal, options?: RequestOptions): Promise<T>
718
+ ```
719
+
720
+ Remove something.
721
+
722
+ NOT RETRIED, for the same reason as [GemClient.put](https://gemarcade.com/developer/api/server#gemclient-put), and widened the
723
+ same way.
724
+
725
+ | Parameter | Type | Description |
726
+ | --- | --- | --- |
727
+ | `path` | `string` | From the API root. |
728
+ | `signal` | `AbortSignal` | Cancels the call. |
729
+ | `options` | `RequestOptions` | See [RequestOptions](https://gemarcade.com/developer/api/server#requestoptions). |
730
+
731
+ **Throws.** [GemApiError](https://gemarcade.com/developer/api/server#gemapierror) on a refusal.
732
+
733
+ ## AuthFailure
734
+
735
+ [AuthFailure](https://gemarcade.com/developer/api/server#authfailure)
736
+
737
+ ```ts
738
+ type AuthFailure = "chain_exhausted" | "unavailable" | "timeout" | "internal"
739
+ ```
740
+
741
+ Why the session could not produce a usable token.
742
+
743
+ THIS IS THE CAUSE, NOT A VERDICT. It deliberately never distinguishes a
744
+ sign-out from a ban from a revocation, and it does not tell you whether to
745
+ try again — [AuthState](https://gemarcade.com/developer/api/server#authstate) does that. A game that branches on the reason
746
+ is guessing.
747
+
748
+ - `chain_exhausted` — the token chain cannot be renewed any further.
749
+ - `unavailable` — no token right now.
750
+ - `timeout` — the arcade did not answer in time.
751
+ - `internal` — something went wrong on this side.
752
+
753
+ ## AuthState
754
+
755
+ [AuthState](https://gemarcade.com/developer/api/server#authstate)
756
+
757
+ ```ts
758
+ type AuthState = "ready" | "pending" | "degraded" | "lost"
759
+ ```
760
+
761
+ What the session can currently do, delivered to `GemOptions.onAuthStateChange`.
762
+
763
+ THIS IS THE ONE TO BRANCH ON, and the only route to it: the token source
764
+ itself is internal, so a game cannot read this state by asking for it. Watch
765
+ the callback instead.
766
+
767
+ - `ready` — a usable token is held.
768
+ - `pending` — no token yet, or one is being fetched.
769
+ - `degraded` — the last attempt failed but another may succeed. Keep playing;
770
+ a retry is worthwhile.
771
+ - `lost` — terminal for this session. Signing back in is the only way
772
+ forward, and retrying will not produce one.
773
+
774
+ ## TokenGrant
775
+
776
+ [TokenGrant](https://gemarcade.com/developer/api/server#tokengrant)
777
+
778
+ ```ts
779
+ interface TokenGrant
780
+ ```
781
+
782
+ A credential and the two deadlines on it.
783
+
784
+ YOU DO NOT HANDLE ONE IN NORMAL USE. The SDK obtains it, refreshes it before
785
+ it expires, and attaches it to every call. It is named here because the
786
+ token-source signatures a bundle reads when wiring things up itself mention
787
+ it.
788
+
789
+ TWO DEADLINES, AND THEY MEAN DIFFERENT THINGS. `expiresAt` is when this
790
+ credential stops working and a fresh one is fetched — routine, invisible.
791
+ `chainEndsAt` is when fetching a fresh one stops being possible at all: past
792
+ it the session is over and only signing in again helps.
793
+
794
+ ## TokenGrant.token
795
+
796
+ [TokenGrant.token](https://gemarcade.com/developer/api/server#tokengrant-token)
797
+
798
+ ```ts
799
+ readonly token: string
800
+ ```
801
+
802
+ The credential itself.
803
+
804
+ NEVER LOG IT AND NEVER FORWARD IT. Anything holding it can act as this
805
+ session until it expires.
806
+
807
+ ## TokenGrant.expiresAt
808
+
809
+ [TokenGrant.expiresAt](https://gemarcade.com/developer/api/server#tokengrant-expiresat)
810
+
811
+ ```ts
812
+ readonly expiresAt: number
813
+ ```
814
+
815
+ When this credential stops working, in seconds since the Unix epoch.
816
+
817
+ Routine — the SDK renews before this, and you do not see it happen.
818
+
819
+ ## TokenGrant.chainEndsAt
820
+
821
+ [TokenGrant.chainEndsAt](https://gemarcade.com/developer/api/server#tokengrant-chainendsat)
822
+
823
+ ```ts
824
+ readonly chainEndsAt: number
825
+ ```
826
+
827
+ When renewal stops being possible, in seconds since the Unix epoch.
828
+
829
+ Past this the session cannot be extended by anything. Watch
830
+ [AuthState](https://gemarcade.com/developer/api/server#authstate) rather than this number.
831
+
832
+ ## AuthError
833
+
834
+ [AuthError](https://gemarcade.com/developer/api/server#autherror)
835
+
836
+ ```ts
837
+ class AuthError
838
+ ```
839
+
840
+ The session cannot produce a usable token.
841
+
842
+ Thrown by any call that needs the player's session. You catch this; you never
843
+ construct it.
844
+
845
+ IT CARRIES NO `retryable` FIELD, and that is deliberate rather than an
846
+ oversight — [AuthError.reason](https://gemarcade.com/developer/api/server#autherror-reason) is the cause, and the retry verdict
847
+ lives on the session's state. Watch `GemOptions.onAuthStateChange` and treat
848
+ [AuthState](https://gemarcade.com/developer/api/server#authstate) `degraded` as worth retrying and `lost` as terminal. A game
849
+ that decides from the reason will retry a session that can never come back.
850
+
851
+ ## AuthError.reason
852
+
853
+ [AuthError.reason](https://gemarcade.com/developer/api/server#autherror-reason)
854
+
855
+ ```ts
856
+ readonly reason: AuthFailure
857
+ ```
858
+
859
+ Why the token could not be produced. See [AuthFailure](https://gemarcade.com/developer/api/server#authfailure) — it is the
860
+ cause, and not a signal about whether to try again.
861
+
862
+ ## AuthError.isAuthError
863
+
864
+ [AuthError.isAuthError](https://gemarcade.com/developer/api/server#autherror-isautherror)
865
+
866
+ ```ts
867
+ isAuthError(error: unknown): error is AuthError
868
+ ```
869
+
870
+ Whether a caught value is an `AuthError`.
871
+
872
+ Prefer this to `instanceof`. Two copies of the SDK can coexist on a page —
873
+ the arcade vendors one build and games install another — and an error from
874
+ one copy fails `instanceof` in the other. This check does not care which
875
+ copy minted it.
876
+
877
+ ```js
878
+ try {
879
+ await somePlatformCall();
880
+ } catch (err) {
881
+ if (AuthError.isAuthError(err)) {
882
+ // the session is the problem; watch onAuthStateChange for whether it recovers
883
+ }
884
+ }
885
+ ```
886
+
887
+ The call is left unnamed on purpose: this error reaches a game and a server
888
+ bundle alike, and the two have no call in common.
889
+
890
+ | Parameter | Type |
891
+ | --- | --- |
892
+ | `error` | `unknown` |
893
+
894
+ ## TransportRefusal
895
+
896
+ [TransportRefusal](https://gemarcade.com/developer/api/server#transportrefusal)
897
+
898
+ ```ts
899
+ class TransportRefusal
900
+ ```
901
+
902
+ The request was never sent, because sending it would have been wrong.
903
+
904
+ ALWAYS YOUR BUG, never a transient fault, and never worth retrying — the same
905
+ call will be refused identically every time. Seven things produce one: a path
906
+ or base URL that will not parse, a path that changes the protocol or host, a
907
+ path or base URL carrying credentials, a body that will not serialise to
908
+ JSON, a raw body that is not bytes or is sent alongside a JSON body, a
909
+ conditional-header value the wire cannot carry, and a retry count outside
910
+ 0..10.
911
+
912
+ Nothing reached the platform, so there is no status and no response to read.
913
+ Fix the call.
914
+
915
+ ## TransportRefusal.retryable
916
+
917
+ [TransportRefusal.retryable](https://gemarcade.com/developer/api/server#transportrefusal-retryable)
918
+
919
+ ```ts
920
+ readonly retryable: false
921
+ ```
922
+
923
+ Always `false`.
924
+
925
+ Present so that code branching on `retryable` across the errors this SDK
926
+ throws reads a real `false` here rather than `undefined`. A refusal is
927
+ deterministic; retrying cannot change it.
928
+
929
+ ## TransportRefusal.isRefusal
930
+
931
+ [TransportRefusal.isRefusal](https://gemarcade.com/developer/api/server#transportrefusal-isrefusal)
932
+
933
+ ```ts
934
+ isRefusal(error: unknown): error is TransportRefusal
935
+ ```
936
+
937
+ Whether a caught value is a `TransportRefusal`.
938
+
939
+ Prefer this to `instanceof`. Two copies of the SDK can coexist on a page —
940
+ the arcade vendors one build and games install another — and an error from
941
+ one copy fails `instanceof` in the other.
942
+
943
+ | Parameter | Type |
944
+ | --- | --- |
945
+ | `error` | `unknown` |
946
+
947
+ ## CONDITIONAL_HEADERS
948
+
949
+ [CONDITIONAL_HEADERS](https://gemarcade.com/developer/api/server#conditional_headers)
950
+
951
+ ```ts
952
+ CONDITIONAL_HEADERS: readonly ["if-match", "if-none-match"]
953
+ ```
954
+
955
+ The only request headers a call may set: `if-match` and `if-none-match`.
956
+
957
+ A CLOSED SET, ON PURPOSE. Everything else — the credential, the content type,
958
+ the client's identity — is the SDK's to send, and a call that could add its
959
+ own headers could send headers the platform trusts.
960
+
961
+ ## ConditionalRequestHeaders
962
+
963
+ [ConditionalRequestHeaders](https://gemarcade.com/developer/api/server#conditionalrequestheaders)
964
+
965
+ ```ts
966
+ type ConditionalRequestHeaders = Partial<Record<unknown[number], string>>
967
+ ```
968
+
969
+ The type of [TransportRequest.headers](https://gemarcade.com/developer/api/server#transportrequest-headers) — derived from
970
+ [CONDITIONAL_HEADERS](https://gemarcade.com/developer/api/server#conditional_headers), so the two cannot disagree.
971
+
972
+ ## TransportRequest
973
+
974
+ [TransportRequest](https://gemarcade.com/developer/api/server#transportrequest)
975
+
976
+ ```ts
977
+ interface TransportRequest
978
+ ```
979
+
980
+ One outgoing call, as a [Transport](https://gemarcade.com/developer/api/server#transport) receives it.
981
+
982
+ You meet this when you write a transport — usually in a test, to assert what
983
+ your code sends without a network.
984
+
985
+ ## TransportRequest.method
986
+
987
+ [TransportRequest.method](https://gemarcade.com/developer/api/server#transportrequest-method)
988
+
989
+ ```ts
990
+ readonly method: "GET" | "PUT" | "POST" | "PATCH" | "DELETE"
991
+ ```
992
+
993
+ The HTTP method.
994
+
995
+ ## TransportRequest.path
996
+
997
+ [TransportRequest.path](https://gemarcade.com/developer/api/server#transportrequest-path)
998
+
999
+ ```ts
1000
+ readonly path: string
1001
+ ```
1002
+
1003
+ The path, from the API root — `/v1/players/me/settings`. Never a full URL:
1004
+ where it is sent is not a call's decision.
1005
+
1006
+ ## TransportRequest.body
1007
+
1008
+ [TransportRequest.body](https://gemarcade.com/developer/api/server#transportrequest-body)
1009
+
1010
+ ```ts
1011
+ readonly body?: unknown
1012
+ ```
1013
+
1014
+ The request body, if there is one. Encoded as JSON.
1015
+
1016
+ ## TransportRequest.bytes
1017
+
1018
+ [TransportRequest.bytes](https://gemarcade.com/developer/api/server#transportrequest-bytes)
1019
+
1020
+ ```ts
1021
+ readonly bytes?: Uint8Array<ArrayBufferLike>
1022
+ ```
1023
+
1024
+ A raw request body, sent as it is and labelled `application/octet-stream`.
1025
+
1026
+ For a route whose input is your own bytes rather than JSON. Set this or
1027
+ [TransportRequest.body](https://gemarcade.com/developer/api/server#transportrequest-body), never both: a request carrying both is
1028
+ refused rather than sent with one of them dropped.
1029
+
1030
+ ## TransportRequest.responseType
1031
+
1032
+ [TransportRequest.responseType](https://gemarcade.com/developer/api/server#transportrequest-responsetype)
1033
+
1034
+ ```ts
1035
+ readonly responseType?: "json" | "bytes"
1036
+ ```
1037
+
1038
+ How to read a successful answer: as JSON (the default), or as the raw
1039
+ bytes that arrived.
1040
+
1041
+ Only a success is read as bytes. A refusal is the platform's own JSON
1042
+ envelope whichever you ask for, so its code and message still reach the
1043
+ error you catch.
1044
+
1045
+ ## TransportRequest.headers
1046
+
1047
+ [TransportRequest.headers](https://gemarcade.com/developer/api/server#transportrequest-headers)
1048
+
1049
+ ```ts
1050
+ readonly headers?: Partial<Record<"if-match" | "if-none-match", string>>
1051
+ ```
1052
+
1053
+ See [ConditionalRequestHeaders](https://gemarcade.com/developer/api/server#conditionalrequestheaders) — nothing else may be set.
1054
+
1055
+ ## TransportRequest.signal
1056
+
1057
+ [TransportRequest.signal](https://gemarcade.com/developer/api/server#transportrequest-signal)
1058
+
1059
+ ```ts
1060
+ readonly signal?: AbortSignal
1061
+ ```
1062
+
1063
+ Cancels the request. Timeouts are the caller's policy, not the SDK's.
1064
+
1065
+ ## TransportRequest.keepalive
1066
+
1067
+ [TransportRequest.keepalive](https://gemarcade.com/developer/api/server#transportrequest-keepalive)
1068
+
1069
+ ```ts
1070
+ readonly keepalive?: boolean
1071
+ ```
1072
+
1073
+ Let the request outlive the page.
1074
+
1075
+ For the write fired as a player leaves. Without this, a request started
1076
+ while the page is unloading is simply cancelled — it looks like it was sent,
1077
+ and nothing arrives.
1078
+
1079
+ NOT ON EVERYTHING, because browsers cap how much can be in flight this way
1080
+ (64 KB across all such requests). Use it for the write that matters.
1081
+
1082
+ ## TransportResponse
1083
+
1084
+ [TransportResponse](https://gemarcade.com/developer/api/server#transportresponse)
1085
+
1086
+ ```ts
1087
+ interface TransportResponse
1088
+ ```
1089
+
1090
+ What a [Transport](https://gemarcade.com/developer/api/server#transport) answers with.
1091
+
1092
+ THREE HEADERS ARE PARSED OUT and there is no header bag, because these are the
1093
+ only three anything above this point reads — which also keeps a stub in a test
1094
+ to a few fields.
1095
+
1096
+ ## TransportResponse.status
1097
+
1098
+ [TransportResponse.status](https://gemarcade.com/developer/api/server#transportresponse-status)
1099
+
1100
+ ```ts
1101
+ readonly status: number
1102
+ ```
1103
+
1104
+ The HTTP status.
1105
+
1106
+ ## TransportResponse.body
1107
+
1108
+ [TransportResponse.body](https://gemarcade.com/developer/api/server#transportresponse-body)
1109
+
1110
+ ```ts
1111
+ readonly body: unknown
1112
+ ```
1113
+
1114
+ The decoded response body — or, for a success when the request asked for
1115
+ `responseType: 'bytes'`, a `Uint8Array` of exactly what arrived.
1116
+
1117
+ ## TransportResponse.etag
1118
+
1119
+ [TransportResponse.etag](https://gemarcade.com/developer/api/server#transportresponse-etag)
1120
+
1121
+ ```ts
1122
+ readonly etag?: string
1123
+ ```
1124
+
1125
+ The resource's version, parsed out of the response.
1126
+
1127
+ Absent when the server sent none — not every resource is versioned. If you
1128
+ are writing a transport for a test, set it: leaving it out makes every
1129
+ conditional read look unversioned.
1130
+
1131
+ ## TransportResponse.retryAfterSeconds
1132
+
1133
+ [TransportResponse.retryAfterSeconds](https://gemarcade.com/developer/api/server#transportresponse-retryafterseconds)
1134
+
1135
+ ```ts
1136
+ readonly retryAfterSeconds?: number
1137
+ ```
1138
+
1139
+ How long the server asked you to wait, in WHOLE SECONDS.
1140
+
1141
+ Seconds, not milliseconds — do not divide it by anything.
1142
+
1143
+ ABSENT MEANS "NO ESTIMATE", NEVER "DO NOT RETRY". Whether a refusal is
1144
+ worth repeating is [GemApiError.retryable](https://gemarcade.com/developer/api/server#gemapierror-retryable)'s answer, not this field's.
1145
+
1146
+ ## TransportResponse.invocationId
1147
+
1148
+ [TransportResponse.invocationId](https://gemarcade.com/developer/api/server#transportresponse-invocationid)
1149
+
1150
+ ```ts
1151
+ readonly invocationId?: string
1152
+ ```
1153
+
1154
+ The platform's id for the invocation this call started, when it started
1155
+ one — a serverless function call is the one that does.
1156
+
1157
+ Absent when the answer named none, and also when the runtime could not read
1158
+ the header: a browser only reads the response headers the platform exposes
1159
+ to it.
1160
+
1161
+ ## Transport
1162
+
1163
+ [Transport](https://gemarcade.com/developer/api/server#transport)
1164
+
1165
+ ```ts
1166
+ interface Transport
1167
+ ```
1168
+
1169
+ How calls actually leave — the seam you replace to test without a network.
1170
+
1171
+ The SDK builds a real one for you. Pass your own when you want to assert what
1172
+ your code sends, or to answer it without a server.
1173
+
1174
+ ```js
1175
+ const sent = [];
1176
+ const transport = {
1177
+ async request(req) {
1178
+ sent.push(req);
1179
+ return { status: 200, body: { ok: true } };
1180
+ },
1181
+ };
1182
+ ```
1183
+
1184
+ ## Transport.request
1185
+
1186
+ [Transport.request](https://gemarcade.com/developer/api/server#transport-request)
1187
+
1188
+ ```ts
1189
+ request(request: TransportRequest): Promise<TransportResponse>
1190
+ ```
1191
+
1192
+ Send one request and resolve with what came back.
1193
+
1194
+ | Parameter | Type |
1195
+ | --- | --- |
1196
+ | `request` | `TransportRequest` |
1197
+
1198
+ ## ChallengeWindow
1199
+
1200
+ [ChallengeWindow](https://gemarcade.com/developer/api/server#challengewindow)
1201
+
1202
+ ```ts
1203
+ type ChallengeWindow = "daily" | "weekly" | "monthly" | "cron" | string & object
1204
+ ```
1205
+
1206
+ How often a challenge resets.
1207
+
1208
+ Open on purpose: the platform may add a window, and a game that pattern-
1209
+ matches on the three it knows should fall through rather than fail to
1210
+ compile against a newer SDK.
1211
+
1212
+ ## Challenge
1213
+
1214
+ [Challenge](https://gemarcade.com/developer/api/server#challenge)
1215
+
1216
+ ```ts
1217
+ interface Challenge
1218
+ ```
1219
+
1220
+ One challenge, in one period.
1221
+
1222
+ Everything here is a function of your game config, the calendar and the
1223
+ player's time zone. It says nothing about the player's progress or streak.
1224
+
1225
+ ⚠️ **Key a period on [Challenge.periodLocalDate](https://gemarcade.com/developer/api/server#challenge-periodlocaldate), or on
1226
+ [Challenge.seed](https://gemarcade.com/developer/api/server#challenge-seed) for a `cron` challenge — never on
1227
+ [Challenge.periodStartAt](https://gemarcade.com/developer/api/server#challenge-periodstartat).** Two players on the same date, and on the
1228
+ same board, see different start instants when their time zones differ.
1229
+
1230
+ ## Challenge.id
1231
+
1232
+ [Challenge.id](https://gemarcade.com/developer/api/server#challenge-id)
1233
+
1234
+ ```ts
1235
+ readonly id: string
1236
+ ```
1237
+
1238
+ The challenge's permanent id — the progression's id from your game config.
1239
+
1240
+ Stable across renames, and the thing your own content should be keyed on.
1241
+ A challenge's id means one thing forever — reusing it for a different
1242
+ challenge merges two histories into one; the daily-challenges guide, §7,
1243
+ is the page.
1244
+
1245
+ ## Challenge.slug
1246
+
1247
+ [Challenge.slug](https://gemarcade.com/developer/api/server#challenge-slug)
1248
+
1249
+ ```ts
1250
+ readonly slug?: string
1251
+ ```
1252
+
1253
+ The slug from your game config, when you declared one.
1254
+
1255
+ Absent means absent — the field is omitted, never `null` or `''`.
1256
+
1257
+ ## Challenge.window
1258
+
1259
+ [Challenge.window](https://gemarcade.com/developer/api/server#challenge-window)
1260
+
1261
+ ```ts
1262
+ readonly window: ChallengeWindow
1263
+ ```
1264
+
1265
+ How often this challenge resets.
1266
+
1267
+ ## Challenge.periodStartAt
1268
+
1269
+ [Challenge.periodStartAt](https://gemarcade.com/developer/api/server#challenge-periodstartat)
1270
+
1271
+ ```ts
1272
+ readonly periodStartAt: number
1273
+ ```
1274
+
1275
+ When this period began for this player, in Unix seconds.
1276
+
1277
+ ⚠️ **Informational: never key anything on it** — not content, not a cache,
1278
+ not a result you send your server. A `local` period begins at the player's
1279
+ own reset time, so the same date starts at a different instant in each
1280
+ time zone, and an earlier period (`periodsBack`) is computed in the zone
1281
+ the player has now. Key on [Challenge.periodLocalDate](https://gemarcade.com/developer/api/server#challenge-periodlocaldate), or on
1282
+ [Challenge.seed](https://gemarcade.com/developer/api/server#challenge-seed) for a `cron` challenge.
1283
+
1284
+ ## Challenge.periodEndsAt
1285
+
1286
+ [Challenge.periodEndsAt](https://gemarcade.com/developer/api/server#challenge-periodendsat)
1287
+
1288
+ ```ts
1289
+ readonly periodEndsAt: number
1290
+ ```
1291
+
1292
+ When this period stops accepting progress, in Unix seconds.
1293
+
1294
+ **This is the seed's expiry.** After this instant the challenge is in a new
1295
+ period with a different [Challenge.seed](https://gemarcade.com/developer/api/server#challenge-seed); read `gem.challenges.list`
1296
+ again rather than computing the next boundary.
1297
+
1298
+ ⚠️ **Comparing it against the device clock is the one place that clock
1299
+ still matters** — and the consequence of a wrong one is bounded: you read
1300
+ late and show yesterday's subject a while longer. It cannot put your
1301
+ progress in the wrong period, because the platform decides that from the
1302
+ instant the write arrives, not from anything your game sends.
1303
+
1304
+ ⚠️ **Clamp any timer you build from it.** This is `0` when the platform
1305
+ sent something this SDK could not read (see below), and
1306
+ `setTimeout(refresh, (periodEndsAt - now) * 1000)` on a negative delay
1307
+ fires immediately, every time — a tight poll with no backoff. A
1308
+ `Math.max(60_000, ...)` floor turns that into a slow retry.
1309
+
1310
+ ## Challenge.periodLocalDate
1311
+
1312
+ [Challenge.periodLocalDate](https://gemarcade.com/developer/api/server#challenge-periodlocaldate)
1313
+
1314
+ ```ts
1315
+ readonly periodLocalDate?: string
1316
+ ```
1317
+
1318
+ The period's calendar date, `YYYY-MM-DD`: the day for a daily, the Monday
1319
+ for a weekly, the 1st for a monthly, in the calendar the challenge runs on.
1320
+
1321
+ **This is the period's identity, and what to key on** — together with
1322
+ [Challenge.id](https://gemarcade.com/developer/api/server#challenge-id), for content, a cache, or a result your server checks.
1323
+ Every player on the same date shares the same [Challenge.seed](https://gemarcade.com/developer/api/server#challenge-seed) and
1324
+ the same stored content, whatever their time zone.
1325
+
1326
+ Absent — never `''` or `null` — for a `cron` challenge, which has no
1327
+ calendar date, and when the platform sent no date this SDK would pass on;
1328
+ key on [Challenge.seed](https://gemarcade.com/developer/api/server#challenge-seed) then: `c.periodLocalDate ?? c.seed`.
1329
+
1330
+ ## Challenge.boundary
1331
+
1332
+ [Challenge.boundary](https://gemarcade.com/developer/api/server#challenge-boundary)
1333
+
1334
+ ```ts
1335
+ readonly boundary: string
1336
+ ```
1337
+
1338
+ Where this challenge's calendar runs: `'local'` — the player's own time
1339
+ zone — or the zone your game config fixed for everybody, as an IANA name
1340
+ such as `'UTC'` or `'Asia/Tokyo'`. `'UTC'` for a `cron` challenge.
1341
+
1342
+ A player with no time zone stored on the platform is on UTC, so a `local`
1343
+ challenge is on UTC for them.
1344
+
1345
+ `''` when the platform sent something this SDK would not pass on.
1346
+
1347
+ ## Challenge.resetTime
1348
+
1349
+ [Challenge.resetTime](https://gemarcade.com/developer/api/server#challenge-resettime)
1350
+
1351
+ ```ts
1352
+ readonly resetTime: string
1353
+ ```
1354
+
1355
+ The clock time, `HH:MM` in [Challenge.boundary](https://gemarcade.com/developer/api/server#challenge-boundary)'s zone, at which a
1356
+ new period begins. `'00:00'` unless your game config set one, and always
1357
+ `'00:00'` for a `cron` challenge, whose periods begin when its expression
1358
+ fires.
1359
+
1360
+ `''` when the platform sent something this SDK would not pass on.
1361
+
1362
+ ## Challenge.seed
1363
+
1364
+ [Challenge.seed](https://gemarcade.com/developer/api/server#challenge-seed)
1365
+
1366
+ ```ts
1367
+ readonly seed: string
1368
+ ```
1369
+
1370
+ The subject selector for this challenge in this period.
1371
+
1372
+ Identical for every player in this game and period — **in every channel**,
1373
+ because the channel is not part of what the platform derives it from — and
1374
+ different in the next period. For a daily, weekly or monthly challenge the
1375
+ period is the [Challenge.periodLocalDate](https://gemarcade.com/developer/api/server#challenge-periodlocaldate), so players in different
1376
+ time zones share each board, each reaching it when their own period begins.
1377
+
1378
+ Lowercase hex, and **a string on purpose**: JavaScript numbers are doubles,
1379
+ so a 64-bit seed parsed as a number loses its low bits and two clients
1380
+ rounding differently disagree about today's board. Use it as text, or parse
1381
+ only as much of it as your generator needs. **Treat the length as opaque**
1382
+ — take a prefix, never assume one.
1383
+
1384
+ ⚠️ **IT IS NOT A SECRET, and nothing may depend on it being one.** It is
1385
+ derived from public inputs and is entirely predictable: anyone can compute
1386
+ the next one, and every player's client already holds this one. Never
1387
+ authorize anything with it, never use it where a value must be
1388
+ unguessable, and if knowing the subject in advance would break your game,
1389
+ the subject has to come from your own server instead. Its job is
1390
+ agreement, not secrecy.
1391
+
1392
+ ⚠️ **`''` means the platform sent something this SDK would not pass on** —
1393
+ a value outside the lowercase-hex contract above. It is falsy, so
1394
+ `if (today.seed)` is the guard; do not start a seeded board without one.
1395
+
1396
+ ## Challenge.data
1397
+
1398
+ [Challenge.data](https://gemarcade.com/developer/api/server#challenge-data)
1399
+
1400
+ ```ts
1401
+ readonly data?: string
1402
+ ```
1403
+
1404
+ The content your studio pre-generated for this challenge and period —
1405
+ usually JSON describing today's board — when it stored some.
1406
+
1407
+ ⚠️ **It may be absent, and your game must keep working when it is.** The
1408
+ field is omitted — never `''` — when nothing is stored for this period, and
1409
+ then you fall back to [Challenge.seed](https://gemarcade.com/developer/api/server#challenge-seed) or a pool bundled with your
1410
+ game. Content is written ahead of time by your studio's producer (a CI job,
1411
+ a cron job, an agent, or a developer with `gemctl challenges data push`),
1412
+ and a producer's runway can run out.
1413
+
1414
+ It is opaque to the platform, which never parses it: parse it yourself, and
1415
+ treat a value you cannot parse the way you treat an absent one.
1416
+
1417
+ ⚠️ **It is not per channel.** Content is stored once for the challenge and
1418
+ period and served on every channel whose config displays that challenge —
1419
+ the same bytes on `dev` as on `release`. Content pushed to try out on `dev`
1420
+ reaches `release` players as soon as the `release` config displays the same
1421
+ challenge [Challenge.id](https://gemarcade.com/developer/api/server#challenge-id); to trial different content, give the trial
1422
+ challenge a different id. The player's progress on it stays per channel.
1423
+
1424
+ ⚠️ **It is not authority.** Every player in the period receives the same
1425
+ bytes, so it must never hold anything a verifier relies on being secret —
1426
+ a hidden solution stays on your server. And it must never hold personal
1427
+ data.
1428
+
1429
+ A period's content becomes readable only once that period has begun.
1430
+
1431
+ ## Challenge.dataHash
1432
+
1433
+ [Challenge.dataHash](https://gemarcade.com/developer/api/server#challenge-datahash)
1434
+
1435
+ ```ts
1436
+ readonly dataHash?: string
1437
+ ```
1438
+
1439
+ 16 lowercase hex identifying [Challenge.data](https://gemarcade.com/developer/api/server#challenge-data) — present exactly when
1440
+ `data` is.
1441
+
1442
+ **Send it with a result, together with [Challenge.periodLocalDate](https://gemarcade.com/developer/api/server#challenge-periodlocaldate)**
1443
+ — or [Challenge.seed](https://gemarcade.com/developer/api/server#challenge-seed) for a `cron` challenge, which has no date.
1444
+ A verifier reads the same period and compares hashes, which is how it
1445
+ proves the player played the content it is checking against. It changes
1446
+ only when your studio corrects a running period; clients may hold the old
1447
+ content for up to an hour afterwards, and the hash is how a verifier tells
1448
+ them apart.
1449
+
1450
+ ## FunctionInvokeOptions
1451
+
1452
+ [FunctionInvokeOptions](https://gemarcade.com/developer/api/server#functioninvokeoptions)
1453
+
1454
+ ```ts
1455
+ interface FunctionInvokeOptions
1456
+ ```
1457
+
1458
+ What [FunctionsApi.invoke](https://gemarcade.com/developer/api/server#functionsapi-invoke) takes besides the input.
1459
+
1460
+ ## FunctionInvokeOptions.signal
1461
+
1462
+ [FunctionInvokeOptions.signal](https://gemarcade.com/developer/api/server#functioninvokeoptions-signal)
1463
+
1464
+ ```ts
1465
+ readonly signal?: AbortSignal
1466
+ ```
1467
+
1468
+ Cancel the call. A call already cancelled sends nothing.
1469
+
1470
+ ## FunctionResult
1471
+
1472
+ [FunctionResult](https://gemarcade.com/developer/api/server#functionresult)
1473
+
1474
+ ```ts
1475
+ interface FunctionResult
1476
+ ```
1477
+
1478
+ Your function's answer: its bytes, and ways to read them.
1479
+
1480
+ ```js
1481
+ const result = await gem.functions.invoke({ v: 2, runId, finishMs });
1482
+ const verdict = result.json();
1483
+ ```
1484
+
1485
+ ## FunctionResult.bytes
1486
+
1487
+ [FunctionResult.bytes](https://gemarcade.com/developer/api/server#functionresult-bytes)
1488
+
1489
+ ```ts
1490
+ readonly bytes: Uint8Array
1491
+ ```
1492
+
1493
+ Exactly the bytes your function returned in `Ok`. Empty when it returned none.
1494
+
1495
+ ## FunctionResult.invocationId
1496
+
1497
+ [FunctionResult.invocationId](https://gemarcade.com/developer/api/server#functionresult-invocationid)
1498
+
1499
+ ```ts
1500
+ readonly invocationId?: string
1501
+ ```
1502
+
1503
+ The platform's id for this call — what to quote when you ask about one call.
1504
+
1505
+ Absent when your code cannot read it: a server bundle can, and a game page
1506
+ can only once the platform lets a page read that response header.
1507
+
1508
+ ## FunctionResult.text
1509
+
1510
+ [FunctionResult.text](https://gemarcade.com/developer/api/server#functionresult-text)
1511
+
1512
+ ```ts
1513
+ text(): string
1514
+ ```
1515
+
1516
+ The answer as UTF-8 text.
1517
+
1518
+ **Throws.** `TypeError` when the bytes are not valid UTF-8, rather than
1519
+ replacing what it cannot decode.
1520
+
1521
+ ## FunctionResult.json
1522
+
1523
+ [FunctionResult.json](https://gemarcade.com/developer/api/server#functionresult-json)
1524
+
1525
+ ```ts
1526
+ json<T>(): T
1527
+ ```
1528
+
1529
+ The answer parsed as JSON.
1530
+
1531
+ **Throws.** `SyntaxError` when the answer is not JSON — including an empty one.
1532
+
1533
+ ## FunctionFailure
1534
+
1535
+ [FunctionFailure](https://gemarcade.com/developer/api/server#functionfailure)
1536
+
1537
+ ```ts
1538
+ type FunctionFailure = "trap" | "memory_exceeded" | "cpu_budget_exceeded" | "host_call_limit"
1539
+ ```
1540
+
1541
+ Why a call that reached your function did not finish — carried on
1542
+ [GemFunctionError.failure](https://gemarcade.com/developer/api/server#gemfunctionerror-failure).
1543
+
1544
+ - `trap` — a panic, an `unreachable`, an out-of-bounds access or a stack
1545
+ overflow. No stack trace comes back.
1546
+ - `memory_exceeded` — past the memory limit.
1547
+ - `cpu_budget_exceeded` — past the CPU budget.
1548
+ - `host_call_limit` — more platform API calls than one invocation may make.
1549
+
1550
+ ## GemFunctionError
1551
+
1552
+ [GemFunctionError](https://gemarcade.com/developer/api/server#gemfunctionerror)
1553
+
1554
+ ```ts
1555
+ class GemFunctionError
1556
+ ```
1557
+
1558
+ A call to your serverless function was refused, or your function did not
1559
+ answer.
1560
+
1561
+ Thrown by [FunctionsApi.invoke](https://gemarcade.com/developer/api/server#functionsapi-invoke) for every refusal the platform sends.
1562
+ It IS a [GemApiError](https://gemarcade.com/developer/api/server#gemapierror), so `reason`, `status`, `code`, `detail` and
1563
+ `retryAfterSeconds` mean what they mean there. Branch on `code` for what
1564
+ happened; `retryable` is decided by the code. You catch this; you never
1565
+ construct it.
1566
+
1567
+ ```js
1568
+ try {
1569
+ show((await gem.functions.invoke(claim)).json());
1570
+ } catch (err) {
1571
+ if (!(err instanceof GemFunctionError)) throw err;
1572
+ if (err.code === 'function_error') reportBug(err.functionMessage, err.invocationId);
1573
+ else if (err.retryable) retryLater(err.retryAfterSeconds ?? 5);
1574
+ else reportBug(err.code, err.invocationId);
1575
+ }
1576
+ ```
1577
+
1578
+ ## GemFunctionError.reason
1579
+
1580
+ [GemFunctionError.reason](https://gemarcade.com/developer/api/server#gemfunctionerror-reason)
1581
+
1582
+ ```ts
1583
+ readonly reason: GemErrorReason
1584
+ ```
1585
+
1586
+ What went wrong, in terms you can act on — `invalid`, `forbidden`,
1587
+ `not_found`, `conflict`, `precondition_failed`, `not_modified`,
1588
+ `insufficient_funds`, `too_large`, `rate_limited` or `unavailable`.
1589
+
1590
+ This is the field to branch on. Two refusals can share a status and mean
1591
+ different things; they do not share a reason.
1592
+
1593
+ ## GemFunctionError.status
1594
+
1595
+ [GemFunctionError.status](https://gemarcade.com/developer/api/server#gemfunctionerror-status)
1596
+
1597
+ ```ts
1598
+ readonly status: number
1599
+ ```
1600
+
1601
+ The HTTP status the server answered with.
1602
+
1603
+ Carried for logs and bug reports. Prefer [GemApiError.reason](https://gemarcade.com/developer/api/server#gemapierror-reason) for
1604
+ decisions: several distinct refusals map onto one status, so branching
1605
+ here treats different problems as the same one.
1606
+
1607
+ ## GemFunctionError.detail
1608
+
1609
+ [GemFunctionError.detail](https://gemarcade.com/developer/api/server#gemfunctionerror-detail)
1610
+
1611
+ ```ts
1612
+ readonly detail: string
1613
+ ```
1614
+
1615
+ The server's own sentence, without the `reason (status):` prefix that
1616
+ `message` carries.
1617
+
1618
+ Use this when you want to quote the platform — in a log line, or a bug
1619
+ report. It is not written for players, so do not put it on screen.
1620
+
1621
+ ## GemFunctionError.retryAfterSeconds
1622
+
1623
+ [GemFunctionError.retryAfterSeconds](https://gemarcade.com/developer/api/server#gemfunctionerror-retryafterseconds)
1624
+
1625
+ ```ts
1626
+ readonly retryAfterSeconds?: number
1627
+ ```
1628
+
1629
+ How long the server asked you to wait, in whole seconds.
1630
+
1631
+ ABSENT MEANS "NO ESTIMATE GIVEN", NEVER "DO NOT RETRY" —
1632
+ [GemApiError.retryable](https://gemarcade.com/developer/api/server#gemapierror-retryable) is what answers that. A refusal can be
1633
+ worth repeating and still name no wait, so treat absence as "pick your
1634
+ own backoff" rather than as a stop signal. The platform estimates a wait
1635
+ for capacity and for rate limits, and sends none for the refusals it
1636
+ cannot put a number on.
1637
+
1638
+ ## GemFunctionError.code
1639
+
1640
+ [GemFunctionError.code](https://gemarcade.com/developer/api/server#gemfunctionerror-code)
1641
+
1642
+ ```ts
1643
+ readonly code?: string
1644
+ ```
1645
+
1646
+ The platform's own error code, when it sent one —
1647
+ `too_many_concurrent_matches`, `cluster_target_required`,
1648
+ `no_compatible_server`.
1649
+
1650
+ Finer-grained than [GemApiError.reason](https://gemarcade.com/developer/api/server#gemapierror-reason), and the only way to tell
1651
+ apart two refusals that share both a reason and a status. Absent on most
1652
+ refusals, so treat it as extra information rather than something to
1653
+ depend on.
1654
+
1655
+ ON A PURCHASE IT IS THE FIELD TO BRANCH ON. `gem.me.mutations.purchase`
1656
+ refusals carry the platform's purchase codes — `insufficient_funds`,
1657
+ `recipe_locked`, `price_mismatch`, `not_purchasable`, … — the same
1658
+ strings `gem.purchases.request` answers as its `reason`, so one `switch`
1659
+ on `err.code` covers both purchase paths.
1660
+
1661
+ ## GemFunctionError.invocationId
1662
+
1663
+ [GemFunctionError.invocationId](https://gemarcade.com/developer/api/server#gemfunctionerror-invocationid)
1664
+
1665
+ ```ts
1666
+ readonly invocationId?: string
1667
+ ```
1668
+
1669
+ The platform's id for the invocation, when the call became one and your
1670
+ code can read the id. See [FunctionResult.invocationId](https://gemarcade.com/developer/api/server#functionresult-invocationid).
1671
+
1672
+ ## GemFunctionError.functionMessage
1673
+
1674
+ [GemFunctionError.functionMessage](https://gemarcade.com/developer/api/server#gemfunctionerror-functionmessage)
1675
+
1676
+ ```ts
1677
+ readonly functionMessage?: string
1678
+ ```
1679
+
1680
+ The text your function returned in `Err`, on `code` `function_error`.
1681
+
1682
+ Written by your function, which may have repeated what the caller sent, so
1683
+ treat it as untrusted: log it, never render it as markup.
1684
+
1685
+ ## GemFunctionError.failure
1686
+
1687
+ [GemFunctionError.failure](https://gemarcade.com/developer/api/server#gemfunctionerror-failure)
1688
+
1689
+ ```ts
1690
+ readonly failure?: string & object | FunctionFailure
1691
+ ```
1692
+
1693
+ On `code` `function_failed`, which limit or fault ended the call. See
1694
+ [FunctionFailure](https://gemarcade.com/developer/api/server#functionfailure). A value this SDK does not know yet arrives as the
1695
+ platform sent it.
1696
+
1697
+ ## GemFunctionError.retryable
1698
+
1699
+ [GemFunctionError.retryable](https://gemarcade.com/developer/api/server#gemfunctionerror-retryable)
1700
+
1701
+ ```ts
1702
+ get retryable(): boolean
1703
+ ```
1704
+
1705
+ Whether repeating the same call could plausibly get a different answer.
1706
+
1707
+ Decided by `code`. True when the platform could not run the call: it was
1708
+ busy, could not reach the function, or was rate-limiting the caller. False
1709
+ when the function ran — `function_error`, `function_failed`,
1710
+ `function_timeout` — and when nothing will change until someone acts:
1711
+ nothing published, input too large, functions switched off. A lost
1712
+ connection is `retryable` too, but your function may have run.
1713
+
1714
+ ## FunctionsApi
1715
+
1716
+ [FunctionsApi](https://gemarcade.com/developer/api/server#functionsapi)
1717
+
1718
+ ```ts
1719
+ class FunctionsApi
1720
+ ```
1721
+
1722
+ Your game's serverless function. Reached as `gem.functions`, in the browser
1723
+ and in a server bundle.
1724
+
1725
+ ## FunctionsApi.invoke
1726
+
1727
+ [FunctionsApi.invoke](https://gemarcade.com/developer/api/server#functionsapi-invoke)
1728
+
1729
+ ```ts
1730
+ invoke(input: unknown, options?: FunctionInvokeOptions): Promise<FunctionResult>
1731
+ ```
1732
+
1733
+ Call the function published on your channel, and get its answer.
1734
+
1735
+ The input is sent as your function's `input`, byte for byte. Pass a
1736
+ `Uint8Array`, an `ArrayBuffer` or another typed array to send bytes as they
1737
+ are. Pass anything else and it is sent as its JSON text — a string too, so
1738
+ `'ping'` arrives as `"ping"` with its quotes.
1739
+
1740
+ The call is made once. It is never repeated for you, because your function
1741
+ may have granted or written something: see [GemFunctionError.retryable](https://gemarcade.com/developer/api/server#gemfunctionerror-retryable)
1742
+ for whether repeating it could help.
1743
+
1744
+ ```js
1745
+ const result = await gem.functions.invoke({ v: 2, runId, finishMs });
1746
+ const { accepted, why } = result.json();
1747
+ ```
1748
+
1749
+ | Parameter | Type | Description |
1750
+ | --- | --- | --- |
1751
+ | `input` | `unknown` | Bytes, or a value to send as JSON. |
1752
+ | `options` | `FunctionInvokeOptions` | |
1753
+
1754
+ **Throws.** [GemFunctionError](https://gemarcade.com/developer/api/server#gemfunctionerror) for a refusal, including your function's
1755
+ own `Err`. `TransportRefusal` for an input that is neither bytes nor
1756
+ JSON, before anything is sent. `AuthError` if the token cannot be renewed.
1757
+
1758
+ ## Clock
1759
+
1760
+ [Clock](https://gemarcade.com/developer/api/server#clock)
1761
+
1762
+ ```ts
1763
+ interface Clock
1764
+ ```
1765
+
1766
+ Where time comes from — so a test does not have to wait for it.
1767
+
1768
+ Credential refresh, retry backoff and timeouts are all wall-clock behaviour.
1769
+ A test that has to wait ten real seconds to prove a timeout fires is a test
1770
+ nobody runs, so every SDK option that involves time takes one of these.
1771
+
1772
+ Pass [systemClock](https://gemarcade.com/developer/api/server#systemclock) — or nothing, which means the same — to use real
1773
+ time. Pass your own to control it.
1774
+
1775
+ ```js
1776
+ let now = 0;
1777
+ const timers = [];
1778
+ const clock = {
1779
+ now: () => now,
1780
+ setTimeout: (fn, ms) => timers.push({ fn, at: now + ms }),
1781
+ clearTimeout: () => {},
1782
+ };
1783
+ // advance time yourself, and run whatever is due
1784
+ ```
1785
+
1786
+ ## Clock.now
1787
+
1788
+ [Clock.now](https://gemarcade.com/developer/api/server#clock-now)
1789
+
1790
+ ```ts
1791
+ now(): number
1792
+ ```
1793
+
1794
+ The current time, in milliseconds — the same scale as `Date.now()`.
1795
+
1796
+ ## Clock.setTimeout
1797
+
1798
+ [Clock.setTimeout](https://gemarcade.com/developer/api/server#clock-settimeout)
1799
+
1800
+ ```ts
1801
+ setTimeout(fn: () => void, ms: number): unknown
1802
+ ```
1803
+
1804
+ Run `fn` after `ms` milliseconds. Return whatever identifies it; the SDK
1805
+ only ever hands that value back to [Clock.clearTimeout](https://gemarcade.com/developer/api/server#clock-cleartimeout).
1806
+
1807
+ | Parameter | Type |
1808
+ | --- | --- |
1809
+ | `fn` | `() => void` |
1810
+ | `ms` | `number` |
1811
+
1812
+ ## Clock.clearTimeout
1813
+
1814
+ [Clock.clearTimeout](https://gemarcade.com/developer/api/server#clock-cleartimeout)
1815
+
1816
+ ```ts
1817
+ clearTimeout(handle: unknown): void
1818
+ ```
1819
+
1820
+ Cancel a timer created by [Clock.setTimeout](https://gemarcade.com/developer/api/server#clock-settimeout).
1821
+
1822
+ | Parameter | Type |
1823
+ | --- | --- |
1824
+ | `handle` | `unknown` |
1825
+
1826
+ ## systemClock
1827
+
1828
+ [systemClock](https://gemarcade.com/developer/api/server#systemclock)
1829
+
1830
+ ```ts
1831
+ systemClock: Clock
1832
+ ```
1833
+
1834
+ Real time — the [Clock](https://gemarcade.com/developer/api/server#clock) every SDK option defaults to.
1835
+
1836
+ Name it when you want to be explicit, or wrap it when you want to watch what
1837
+ the SDK schedules without replacing the clock entirely.
1838
+
1839
+ ## defineServer
1840
+
1841
+ [defineServer](https://gemarcade.com/developer/api/server#defineserver)
1842
+
1843
+ ```ts
1844
+ function defineServer(setup: (gem: GemMatchServer) => GemServerBundle, options?: GemServerOptions): GemServerFactory
1845
+ ```
1846
+
1847
+ Declare your dedicated server. Default-export the result.
1848
+
1849
+ You pass a setup function; it receives a [GemMatchServer](https://gemarcade.com/developer/api/server#gemmatchserver) — the platform
1850
+ operations, your room, your players — and returns the handlers you want. The
1851
+ credential and its refresh are handled for you, and the SDK is disposed when
1852
+ the match ends.
1853
+
1854
+ WHY A FUNCTION RATHER THAN A PLAIN OBJECT: your handlers need to share state.
1855
+ The closure the setup function creates is where your world lives, alongside
1856
+ `gem`, without threading either through five signatures.
1857
+
1858
+ A THROWN ERROR IS NOT SWALLOWED. The platform logs it and keeps serving —
1859
+ except in `init`, where it fails the match. That is deliberate: a server with
1860
+ a broken handler quietly serving nobody is worse than one that stops.
1861
+
1862
+ | Parameter | Type | Description |
1863
+ | --- | --- | --- |
1864
+ | `setup` | `(gem: GemMatchServer) => GemServerBundle` | Called once. Return the handlers you want from
1865
+ [GemServerBundle](https://gemarcade.com/developer/api/server#gemserverbundle); omit the rest. |
1866
+ | `options` | `GemServerOptions` | Passed through to [createGemServer](https://gemarcade.com/developer/api/server#creategemserver). Normally omit it. |
1867
+
1868
+ **Returns.** The value to default-export from your bundle's entry file.
1869
+
1870
+ ```js
1871
+ import { defineServer } from '@indiegems/gem-web-sdk/sdk';
1872
+
1873
+ export default defineServer((gem) => {
1874
+ let round = 0;
1875
+ return {
1876
+ async init() {
1877
+ const room = await gem.room?.get();
1878
+ gem.log(`room has ${room?.members.length ?? 0} player(s)`);
1879
+ },
1880
+ message(playerId, data, isText) {
1881
+ if (isText && data === 'next') gem.broadcast(`round ${++round}`);
1882
+ },
1883
+ async shutdown(info) {
1884
+ if (info?.reason !== 'signal') await gem.match.end();
1885
+ },
1886
+ };
1887
+ });
1888
+ ```
1889
+
1890
+ **Guides.** [server-bundles](https://gemarcade.com/developer/docs/server-bundles)
1891
+
1892
+ ## createMatchServer
1893
+
1894
+ [createMatchServer](https://gemarcade.com/developer/api/server#creatematchserver)
1895
+
1896
+ ```ts
1897
+ function createMatchServer(host: GemServerHost, options?: GemServerOptions): GemMatchServer
1898
+ ```
1899
+
1900
+ Build the `gem` object yourself, when you are driving the lifecycle by hand.
1901
+
1902
+ MOST BUNDLES WANT [defineServer](https://gemarcade.com/developer/api/server#defineserver), which calls this for you and wires up
1903
+ your handlers. Use this one if you are writing the lifecycle object directly.
1904
+
1905
+ CALL IT FROM `init`, not at module scope — it needs the host, and it builds
1906
+ the platform connection immediately, which is what turns "this platform image
1907
+ is too old" into a startup failure rather than a surprise mid-match. Call
1908
+ `dispose()` from `shutdown`.
1909
+
1910
+ | Parameter | Type | Description |
1911
+ | --- | --- | --- |
1912
+ | `host` | `GemServerHost` | The object the runtime passed to `init`. |
1913
+ | `options` | `GemServerOptions` | Passed through to [createGemServer](https://gemarcade.com/developer/api/server#creategemserver). Normally omit it. |
1914
+
1915
+ **Throws.** When the runtime named no API host, so no call could succeed.
1916
+
1917
+ ```js
1918
+ import { createMatchServer } from '@indiegems/gem-web-sdk/sdk';
1919
+
1920
+ export default () => {
1921
+ let gem;
1922
+ return {
1923
+ async init(host) { gem = createMatchServer(host); },
1924
+ join(playerId) { gem.broadcast(`${playerId} joined`); },
1925
+ async shutdown(info) { if (info?.reason !== 'signal') await gem.match.end(); gem.dispose(); },
1926
+ };
1927
+ };
1928
+ ```
1929
+
1930
+ ## createGemServer
1931
+
1932
+ [createGemServer](https://gemarcade.com/developer/api/server#creategemserver)
1933
+
1934
+ ```ts
1935
+ function createGemServer(host: ServerBundleHost, options?: GemServerOptions): GemServer
1936
+ ```
1937
+
1938
+ Build your server's connection to the platform. Call it once, in `init(host)`.
1939
+
1940
+ Pass the `host` object the runtime handed you and you get back a
1941
+ [GemServer](https://gemarcade.com/developer/api/server#gemserver): the platform operations, the room reads for your own room,
1942
+ and a way to end the match. The credential is taken from the host and kept
1943
+ fresh for you.
1944
+
1945
+ MOST BUNDLES DO NOT CALL THIS. [defineServer](https://gemarcade.com/developer/api/server#defineserver) does it for you and hands
1946
+ you the result. Reach for it directly when you are managing the lifecycle
1947
+ yourself.
1948
+
1949
+ IT THROWS AT STARTUP if the runtime did not name an API to call — an old
1950
+ platform image. That is deliberate: a server that silently cannot reach the
1951
+ platform is worse than one that refuses to start.
1952
+
1953
+ | Parameter | Type | Description |
1954
+ | --- | --- | --- |
1955
+ | `host` | `ServerBundleHost` | The object the runtime passes to `init`. |
1956
+ | `options` | `GemServerOptions` | See [GemServerOptions](https://gemarcade.com/developer/api/server#gemserveroptions) — normally omit it. |
1957
+
1958
+ **Throws.** When the runtime named no API host, so no call could succeed.
1959
+
1960
+ ```js
1961
+ import { createGemServer } from '@indiegems/gem-web-sdk/sdk';
1962
+
1963
+ export async function init(host) {
1964
+ const gem = createGemServer(host);
1965
+ const room = await gem.room?.get();
1966
+ host.log(`match ${gem.identity.matchId} with ${room?.members.length ?? 0}`);
1967
+ }
1968
+ ```
1969
+
1970
+ **Guides.** [server-bundles](https://gemarcade.com/developer/docs/server-bundles)
1971
+
1972
+ ## createHostTokenSource
1973
+
1974
+ [createHostTokenSource](https://gemarcade.com/developer/api/server#createhosttokensource)
1975
+
1976
+ ```ts
1977
+ function createHostTokenSource(host: ServerBundleHost, options: HostTokenSourceOptions): HostTokenSource
1978
+ ```
1979
+
1980
+ Keep the platform credential fresh, without building the rest of the SDK.
1981
+
1982
+ MOST BUNDLES NEVER CALL THIS. [createGemServer](https://gemarcade.com/developer/api/server#creategemserver) does it, and hands you a
1983
+ client that is already authenticated. Reach for it when you are assembling
1984
+ the pieces yourself.
1985
+
1986
+ It subscribes to the host's credential pushes straight away. Call `dispose()`
1987
+ when you are done, or a pending refresh timer will keep your process alive
1988
+ after the match is over.
1989
+
1990
+ | Parameter | Type | Description |
1991
+ | --- | --- | --- |
1992
+ | `host` | `ServerBundleHost` | The object the runtime passed to `init`. |
1993
+ | `options` | `HostTokenSourceOptions` | See [HostTokenSourceOptions](https://gemarcade.com/developer/api/server#hosttokensourceoptions) — `clock` is required. |
1994
+
1995
+ **Returns.** Something you can stop. See [HostTokenSource](https://gemarcade.com/developer/api/server#hosttokensource).
1996
+
1997
+ ## achievementReceipts
1998
+
1999
+ [achievementReceipts](https://gemarcade.com/developer/api/server#achievementreceipts)
2000
+
2001
+ ```ts
2002
+ function achievementReceipts(order: unknown): AchievementReceipt[]
2003
+ ```
2004
+
2005
+ The achievement receipts on an order result: one for each achievement
2006
+ threshold the order crossed — unlocked, already held, or suppressed and why
2007
+ — plus any achievement it unlocked because the value was already past the
2008
+ threshold. Empty when the order crossed none, which is almost every order.
2009
+
2010
+ Pass the value a progression write or `createEntityOrder` returned; for a
2011
+ batch, call it once per entry of the result's `orders`. Anything that is not
2012
+ a well-formed achievement receipt is left out, so the call is safe on any
2013
+ order result.
2014
+
2015
+ The receipts say what happened, once. They are not a trigger to pay a
2016
+ game-side reward on: a retried or replayed order reports nothing, because
2017
+ the threshold is already behind it. Put a game reward in the same
2018
+ threshold's `rewards`, which the platform pays on the crossing itself (and
2019
+ again after a reset, unlike the achievement).
2020
+
2021
+ | Parameter | Type |
2022
+ | --- | --- |
2023
+ | `order` | `unknown` |
2024
+
2025
+ ```ts
2026
+ const order = await gem.api.addEntityProgression('players', playerId, 'boards_cleared', {
2027
+ amount: 1,
2028
+ });
2029
+ for (const r of achievementReceipts(order)) {
2030
+ if (r.outcome === 'unlocked') log(`unlocked ${r.achievementId} (+${r.points})`);
2031
+ }
2032
+ ```
2033
+
2034
+ **Guides.** [achievements](https://gemarcade.com/developer/docs/achievements)
2035
+
2036
+ ## AchievementReceipt
2037
+
2038
+ [AchievementReceipt](https://gemarcade.com/developer/api/server#achievementreceipt)
2039
+
2040
+ ```ts
2041
+ interface AchievementReceipt
2042
+ ```
2043
+
2044
+ What became of one achievement an order reached.
2045
+
2046
+ **Guides.** [achievements](https://gemarcade.com/developer/docs/achievements)
2047
+
2048
+ ## GemMatchServer
2049
+
2050
+ [GemMatchServer](https://gemarcade.com/developer/api/server#gemmatchserver)
2051
+
2052
+ ```ts
2053
+ interface GemMatchServer
2054
+ ```
2055
+
2056
+ The `gem` your bundle holds — everything a dedicated server can do, on one
2057
+ object.
2058
+
2059
+ This is what [defineServer](https://gemarcade.com/developer/api/server#defineserver) hands your setup function. It is a
2060
+ [GemServer](https://gemarcade.com/developer/api/server#gemserver) — the platform operations, your room, ending the match —
2061
+ with the match surface added: the players connected right now, and how to talk
2062
+ to them.
2063
+
2064
+ The allocation is spelled ONE way. There is no `gem.matchId`; it is
2065
+ `gem.identity.matchId`.
2066
+
2067
+ ```js
2068
+ export default defineServer((gem) => ({
2069
+ join(playerId) {
2070
+ const who = gem.profile(playerId);
2071
+ gem.broadcast(`${playerId} joined (seat ${gem.playerNumber(playerId)})`);
2072
+ gem.log(`now ${gem.players().length} connected`);
2073
+ },
2074
+ }));
2075
+ ```
2076
+
2077
+ ## GemServerBundle
2078
+
2079
+ [GemServerBundle](https://gemarcade.com/developer/api/server#gemserverbundle)
2080
+
2081
+ ```ts
2082
+ interface GemServerBundle
2083
+ ```
2084
+
2085
+ The five things that can happen to your server. Define the ones you care
2086
+ about; each may be `async` and is awaited before the next event arrives.
2087
+
2088
+ This is what your setup function returns — see [defineServer](https://gemarcade.com/developer/api/server#defineserver). Events
2089
+ are delivered one at a time, in order, so you never handle two at once and do
2090
+ not need locking around your own state.
2091
+
2092
+ ```js
2093
+ export default defineServer((gem) => {
2094
+ const scores = new Map();
2095
+ return {
2096
+ join(playerId) { scores.set(playerId, 0); },
2097
+ leave(playerId) { scores.delete(playerId); },
2098
+ message(playerId, data, isText) {
2099
+ if (isText) scores.set(playerId, scores.get(playerId) + Number(data));
2100
+ },
2101
+ async shutdown(info) { if (info?.reason !== 'signal') await gem.match.end(); },
2102
+ };
2103
+ });
2104
+ ```
2105
+
2106
+ ## GemServerFactory
2107
+
2108
+ [GemServerFactory](https://gemarcade.com/developer/api/server#gemserverfactory)
2109
+
2110
+ ```ts
2111
+ type GemServerFactory = () => GemServerLifecycle
2112
+ ```
2113
+
2114
+ The type of your bundle's default export — what [defineServer](https://gemarcade.com/developer/api/server#defineserver) returns.
2115
+
2116
+ The platform calls it once, at the start of a match.
2117
+
2118
+ ## GemServerLifecycle
2119
+
2120
+ [GemServerLifecycle](https://gemarcade.com/developer/api/server#gemserverlifecycle)
2121
+
2122
+ ```ts
2123
+ interface GemServerLifecycle
2124
+ ```
2125
+
2126
+ What the platform actually calls — a [GemServerBundle](https://gemarcade.com/developer/api/server#gemserverbundle) after the SDK has
2127
+ wrapped it.
2128
+
2129
+ YOU DO NOT WRITE ONE. [defineServer](https://gemarcade.com/developer/api/server#defineserver) produces it from your bundle. It is
2130
+ named here so you can type a variable holding the result, and so the shape the
2131
+ platform drives is written down somewhere a reader can find it.
2132
+
2133
+ `init` and `shutdown` are required here although both are optional on your
2134
+ bundle: the SDK always defines them, to receive the host and to release the
2135
+ credential subscription when the match is over.
2136
+
2137
+ ## RuntimeShutdownPayload
2138
+
2139
+ [RuntimeShutdownPayload](https://gemarcade.com/developer/api/server#runtimeshutdownpayload)
2140
+
2141
+ ```ts
2142
+ interface RuntimeShutdownPayload
2143
+ ```
2144
+
2145
+ What the platform runtime hands `shutdown`, on images that say why.
2146
+
2147
+ Either spelling of the platform reason is accepted: the runtime's wire
2148
+ name (`platform_reason`) and the SDK's (`endedReason`).
2149
+
2150
+ ## ServerShutdownInfo
2151
+
2152
+ [ServerShutdownInfo](https://gemarcade.com/developer/api/server#servershutdowninfo)
2153
+
2154
+ ```ts
2155
+ interface ServerShutdownInfo
2156
+ ```
2157
+
2158
+ Why your server's `shutdown` is running.
2159
+
2160
+ The reason is the platform runtime's own word, passed through unchanged:
2161
+
2162
+ - `match_ended` — the platform ended the match (yours by `gem.match.end()`,
2163
+ or its own: expired, everybody left, …). `endedReason` names which, in
2164
+ the same vocabulary `matchEnded` uses on the client, when the runtime
2165
+ passed it along.
2166
+ - `idle_timeout` — nobody was connected for the idle window. The platform
2167
+ ends the match itself, so the room reopens; there is nothing to end.
2168
+ - `signal` — the platform is reclaiming the machine. A drain: your players
2169
+ are still here and the match plays out. Do not end it.
2170
+
2171
+ On a LISTEN HOST — the same bundle run in the hosting player's browser by
2172
+ `gem.match.serve()` — two more arrive: `host_migrated` (the platform moved
2173
+ the match to another host; this seat no longer speaks for it) and `stopped`
2174
+ (the hosting game called the deprecated `stop()`, which tells the platform
2175
+ nothing).
2176
+
2177
+ OPEN: a later runtime may add a value. Treat one you do not recognise as
2178
+ "the match is over", which is the safe reading.
2179
+
2180
+ ## GemServerProfile
2181
+
2182
+ [GemServerProfile](https://gemarcade.com/developer/api/server#gemserverprofile)
2183
+
2184
+ ```ts
2185
+ interface GemServerProfile
2186
+ ```
2187
+
2188
+ Who a player is, for display. Safe to hold; think before you relay it.
2189
+
2190
+ Returned by [GemMatchServer.profile](https://gemarcade.com/developer/api/server#gemmatchserver-profile).
2191
+
2192
+ RELAY THE ID, NOT THE NAME. Sending player ids to your clients and letting
2193
+ each one render the name it already has is better than broadcasting names
2194
+ through your server: it is less to send, and it keeps one player's text from
2195
+ reaching every other player's screen through you.
2196
+
2197
+ ## GemServer
2198
+
2199
+ [GemServer](https://gemarcade.com/developer/api/server#gemserver)
2200
+
2201
+ ```ts
2202
+ interface GemServer
2203
+ ```
2204
+
2205
+ Your server's connection to the platform — what [createGemServer](https://gemarcade.com/developer/api/server#creategemserver)
2206
+ returns.
2207
+
2208
+ Build one in `init(host)`, keep it, and call through it for the life of the
2209
+ match. It holds the credential and refreshes it for you; there is nothing to
2210
+ sign, renew or attach.
2211
+
2212
+ ```js
2213
+ import { defineServer } from '@indiegems/gem-web-sdk/sdk';
2214
+
2215
+ export default defineServer((gem) => ({
2216
+ async init() {
2217
+ const room = await gem.room?.get();
2218
+ gem.log(`starting with ${room?.members.length ?? 0} player(s)`);
2219
+ },
2220
+ async shutdown(info) {
2221
+ if (info?.reason !== 'signal') await gem.match.end();
2222
+ },
2223
+ }));
2224
+ ```
2225
+
2226
+ ## GemServerIdentity
2227
+
2228
+ [GemServerIdentity](https://gemarcade.com/developer/api/server#gemserveridentity)
2229
+
2230
+ ```ts
2231
+ interface GemServerIdentity
2232
+ ```
2233
+
2234
+ Which game, channel and match this server was started for.
2235
+
2236
+ You never construct one. The platform decides all of it when it allocates the
2237
+ server, and [GemServer.identity](https://gemarcade.com/developer/api/server#gemserver-identity) is it restated in one place so you do
2238
+ not have to keep reaching back to the host object.
2239
+
2240
+ These are also the ids bound into every call `api` makes, which is why a
2241
+ bundle never types a game id and cannot name another game's.
2242
+
2243
+ ## GemServerOptions
2244
+
2245
+ [GemServerOptions](https://gemarcade.com/developer/api/server#gemserveroptions)
2246
+
2247
+ ```ts
2248
+ interface GemServerOptions
2249
+ ```
2250
+
2251
+ Options for [createGemServer](https://gemarcade.com/developer/api/server#creategemserver). Every one is optional, and a bundle
2252
+ running on the platform normally passes none.
2253
+
2254
+ They exist for the two cases the platform does not cover: pointing a bundle at
2255
+ something other than the API the platform named, and running it in a test.
2256
+
2257
+ ## ServerApi
2258
+
2259
+ [ServerApi](https://gemarcade.com/developer/api/server#serverapi)
2260
+
2261
+ ```ts
2262
+ type ServerApi = unknown
2263
+ ```
2264
+
2265
+ Every platform operation your server can call, already holding its credential.
2266
+
2267
+ This is the surface you spend most of your time in: `gem.api.getGameRoomV2(id)`,
2268
+ `gem.api.createOrder(...)`, and the rest. Each method's arguments and return
2269
+ type come from the platform's own API description, so what you can pass and
2270
+ what you get back are the contract rather than someone's reading of it.
2271
+
2272
+ `game_id` is never a parameter. It is filled in from the allocation — in the
2273
+ path and the query alike — so a bundle cannot address another game's data even
2274
+ by mistake.
2275
+
2276
+ The operation list moves with the platform. An operation added to your game's
2277
+ API appears here without this package changing, and one that is withdrawn
2278
+ stops type-checking rather than failing at runtime.
2279
+
2280
+ **Guides.** [server-bundles](https://gemarcade.com/developer/docs/server-bundles)
2281
+
2282
+ ## ServerBundleHost
2283
+
2284
+ [ServerBundleHost](https://gemarcade.com/developer/api/server#serverbundlehost)
2285
+
2286
+ ```ts
2287
+ interface ServerBundleHost
2288
+ ```
2289
+
2290
+ The part of the host object that identifies the match and carries the
2291
+ credential.
2292
+
2293
+ This is what [createGemServer](https://gemarcade.com/developer/api/server#creategemserver) needs, and nothing more — which is also
2294
+ what makes it easy to stand up in a test: four members instead of the whole
2295
+ host. [GemServerHost](https://gemarcade.com/developer/api/server#gemserverhost) is the same object at full width.
2296
+
2297
+ ## ServerTokenGrant
2298
+
2299
+ [ServerTokenGrant](https://gemarcade.com/developer/api/server#servertokengrant)
2300
+
2301
+ ```ts
2302
+ interface ServerTokenGrant
2303
+ ```
2304
+
2305
+ A credential the platform handed your server, and when it stops working.
2306
+
2307
+ You do not normally touch one. [createGemServer](https://gemarcade.com/developer/api/server#creategemserver) takes the credential
2308
+ from the host and refreshes it for you; this type is here for a bundle wiring
2309
+ the pieces up itself.
2310
+
2311
+ ## HostTokenSource
2312
+
2313
+ [HostTokenSource](https://gemarcade.com/developer/api/server#hosttokensource)
2314
+
2315
+ ```ts
2316
+ interface HostTokenSource
2317
+ ```
2318
+
2319
+ A credential subscription — what [createHostTokenSource](https://gemarcade.com/developer/api/server#createhosttokensource) gives back.
2320
+
2321
+ Deliberately narrow: the only thing you can do with it is stop it. The
2322
+ credential itself is not reachable, because nothing you can call takes one.
2323
+
2324
+ ## GemServerHost
2325
+
2326
+ [GemServerHost](https://gemarcade.com/developer/api/server#gemserverhost)
2327
+
2328
+ ```ts
2329
+ interface GemServerHost
2330
+ ```
2331
+
2332
+ The whole `host` object, as the runtime passes it to your bundle.
2333
+
2334
+ [ServerBundleHost](https://gemarcade.com/developer/api/server#serverbundlehost) is its credential half; everything added here is the
2335
+ match — who is connected, what they sent, and how to answer.
2336
+
2337
+ SEVERAL MEMBERS ARE OPTIONAL BECAUSE THE PLATFORM IMAGE MAY BE OLDER than the
2338
+ member. There is no version to ask for; presence is the signal. Check before
2339
+ calling, and your bundle keeps running on every image rather than only the
2340
+ newest.
2341
+
2342
+ A bundle written with [defineServer](https://gemarcade.com/developer/api/server#defineserver) usually reads [GemMatchServer](https://gemarcade.com/developer/api/server#gemmatchserver)
2343
+ instead, which wraps this.
2344
+
2345
+ ## GemServerHostProfile
2346
+
2347
+ [GemServerHostProfile](https://gemarcade.com/developer/api/server#gemserverhostprofile)
2348
+
2349
+ ```ts
2350
+ interface GemServerHostProfile
2351
+ ```
2352
+
2353
+ A player's display identity, as the platform resolved it for your server.
2354
+
2355
+ EVERY FIELD IS OPTIONAL. The platform may not have all of them for a given
2356
+ player, and that is not an error — fall back to something of your own.
2357
+
2358
+ [GemMatchServer.profile](https://gemarcade.com/developer/api/server#gemmatchserver-profile) is the version you usually want: same
2359
+ information, with the name typed as [PlayerText](https://gemarcade.com/developer/api/server#playertext).
2360
+
2361
+ ## SendOptions
2362
+
2363
+ [SendOptions](https://gemarcade.com/developer/api/server#sendoptions)
2364
+
2365
+ ```ts
2366
+ interface SendOptions
2367
+ ```
2368
+
2369
+ What delivery you are ASKING FOR when you send.
2370
+
2371
+ A request, not a guarantee in either direction: a transport may deliver an
2372
+ unreliable message reliably anyway, and it never does the reverse. What you
2373
+ asked for is carried to the receiver either way, so they can tell the
2374
+ difference.
2375
+
2376
+ ## BroadcastOptions
2377
+
2378
+ [BroadcastOptions](https://gemarcade.com/developer/api/server#broadcastoptions)
2379
+
2380
+ ```ts
2381
+ interface BroadcastOptions
2382
+ ```
2383
+
2384
+ The same delivery choices as [SendOptions](https://gemarcade.com/developer/api/server#sendoptions), plus who to skip.
2385
+
2386
+ ## HostTokenSourceOptions
2387
+
2388
+ [HostTokenSourceOptions](https://gemarcade.com/developer/api/server#hosttokensourceoptions)
2389
+
2390
+ ```ts
2391
+ interface HostTokenSourceOptions
2392
+ ```
2393
+
2394
+ Options for [createHostTokenSource](https://gemarcade.com/developer/api/server#createhosttokensource).
2395
+
2396
+ You need these only if you are wiring the credential up yourself.
2397
+ [createGemServer](https://gemarcade.com/developer/api/server#creategemserver) does it for you.
2398
+
2399
+ ## RoomArgValue
2400
+
2401
+ [RoomArgValue](https://gemarcade.com/developer/api/server#roomargvalue)
2402
+
2403
+ ```ts
2404
+ type RoomArgValue = string | number | boolean | readonly string[] | readonly number[] | readonly boolean[]
2405
+ ```
2406
+
2407
+ What a room setting can be: a string, a number, a boolean, or an array of one
2408
+ of those.
2409
+
2410
+ FLAT. There is no nested-object arm, because a room's settings schema cannot
2411
+ declare one. What you get is what your game's published configuration said a
2412
+ room may be created with, already type-checked by the platform before the
2413
+ match was allocated — so a bundle reading settings does not need to validate
2414
+ them again.
2415
+
2416
+ **Guides.** [rooms-and-matches](https://gemarcade.com/developer/docs/rooms-and-matches)
2417
+
2418
+ ## ServerAnalytics
2419
+
2420
+ [ServerAnalytics](https://gemarcade.com/developer/api/server#serveranalytics)
2421
+
2422
+ ```ts
2423
+ interface ServerAnalytics
2424
+ ```
2425
+
2426
+ Your server's analytics — the events you declared in your game config,
2427
+ attributed to the player they are about.
2428
+
2429
+ Reached as `gem.analytics`. [ServerAnalytics.track](https://gemarcade.com/developer/api/server#serveranalytics-track) is synchronous and
2430
+ never throws: it queues, and the queue goes out on a timer, at fifty events,
2431
+ and whenever you call [ServerAnalytics.flush](https://gemarcade.com/developer/api/server#serveranalytics-flush).
2432
+
2433
+ ⚠️ **Flush before your match ends.** A dedicated server is torn down when the
2434
+ match is over and nothing gets a last word in for it — there is no `pagehide`
2435
+ here. `await gem.analytics.flush()` in `shutdown` is the whole of it.
2436
+
2437
+ ```js
2438
+ export default defineServer((gem) => ({
2439
+ async shutdown() {
2440
+ gem.analytics.track(winner, 'match_result', { outcome: 'win', score: 3100 });
2441
+ await gem.analytics.flush();
2442
+ },
2443
+ }));
2444
+ ```
2445
+
2446
+ **Guides.** [analytics](https://gemarcade.com/developer/docs/analytics)
2447
+
2448
+ ## ServerAnalytics.track
2449
+
2450
+ [ServerAnalytics.track](https://gemarcade.com/developer/api/server#serveranalytics-track)
2451
+
2452
+ ```ts
2453
+ track(playerId: string, name: string, props?: Readonly<Record<string, AnalyticsValue>>): void
2454
+ ```
2455
+
2456
+ Record an event about one player. Synchronous, and it never throws.
2457
+
2458
+ String values become dimensions and numbers become measures, matched
2459
+ against your `analytics_events` declaration server-side — an undeclared
2460
+ name or key comes back as a rejection rather than being stored.
2461
+
2462
+ ⚠️ **The player id is required and is not guessed.** A game-server token
2463
+ names no player, so an event from here has to say who it is about; the
2464
+ platform will not infer it. Take the id from `join()` or `host.players()`.
2465
+
2466
+ ⚠️ `gem_` is reserved AND unavailable to a server: the platform's own
2467
+ events are client-sourced, so one emitted here would be refused
2468
+ `source_not_permitted`. It is refused locally instead.
2469
+
2470
+ | Parameter | Type | Description |
2471
+ | --- | --- | --- |
2472
+ | `playerId` | `string` | The player this event is about. |
2473
+ | `name` | `string` | A declared event id — an `id` from your `analytics_events`. |
2474
+ | `props` | `Readonly<Record<string, AnalyticsValue>>` | Dimensions (strings) and measures (numbers). |
2475
+
2476
+ ## ServerAnalytics.flush
2477
+
2478
+ [ServerAnalytics.flush](https://gemarcade.com/developer/api/server#serveranalytics-flush)
2479
+
2480
+ ```ts
2481
+ flush(signal?: AbortSignal): Promise<AnalyticsFlushResult>
2482
+ ```
2483
+
2484
+ Send what is queued now, and find out what happened to it.
2485
+
2486
+ Resolves with the platform's own answer, INCLUDING per-event rejections, which
2487
+ arrive inside a 200 rather than as an error. A refusal that could clear on
2488
+ its own — a 429, a 503 — keeps the queue and rejects; one that cannot
2489
+ discards the batch, because resending it can only fail the same way.
2490
+
2491
+ | Parameter | Type | Description |
2492
+ | --- | --- | --- |
2493
+ | `signal` | `AbortSignal` | Cancels the call — not the queue. |
2494
+
2495
+ ## ServerAnalytics.pending
2496
+
2497
+ [ServerAnalytics.pending](https://gemarcade.com/developer/api/server#serveranalytics-pending)
2498
+
2499
+ ```ts
2500
+ readonly pending: number
2501
+ ```
2502
+
2503
+ How many events are waiting to go out.
2504
+
2505
+ ## ServerListChallengesOptions
2506
+
2507
+ [ServerListChallengesOptions](https://gemarcade.com/developer/api/server#serverlistchallengesoptions)
2508
+
2509
+ ```ts
2510
+ interface ServerListChallengesOptions
2511
+ ```
2512
+
2513
+ Options for [ServerChallenges.list](https://gemarcade.com/developer/api/server#serverchallenges-list).
2514
+
2515
+ ## ServerListChallengesOptions.playerId
2516
+
2517
+ [ServerListChallengesOptions.playerId](https://gemarcade.com/developer/api/server#serverlistchallengesoptions-playerid)
2518
+
2519
+ ```ts
2520
+ readonly playerId: string
2521
+ ```
2522
+
2523
+ The player whose periods you are asking about. Required.
2524
+
2525
+ Take it from `join()` or `gem.players()`, never from something a client
2526
+ sent you. The platform answers only for a player who plays your game or is
2527
+ in it now, and refuses anyone else with `not_found`.
2528
+
2529
+ ## ServerListChallengesOptions.periodsBack
2530
+
2531
+ [ServerListChallengesOptions.periodsBack](https://gemarcade.com/developer/api/server#serverlistchallengesoptions-periodsback)
2532
+
2533
+ ```ts
2534
+ readonly periodsBack?: number
2535
+ ```
2536
+
2537
+ How many periods back to answer for. `0` — the default — is the period
2538
+ running now.
2539
+
2540
+ ⚠️ **Counted per window**, as in the browser: `1` is yesterday for a
2541
+ daily and last week for a weekly. `1` is also what accepts a result the
2542
+ player finished just before their day rolled over.
2543
+
2544
+ An integer from 0 to 400. Anything else is a `TypeError` before the
2545
+ request is made.
2546
+
2547
+ ## ServerListChallengesOptions.signal
2548
+
2549
+ [ServerListChallengesOptions.signal](https://gemarcade.com/developer/api/server#serverlistchallengesoptions-signal)
2550
+
2551
+ ```ts
2552
+ readonly signal?: AbortSignal
2553
+ ```
2554
+
2555
+ Abort the request.
2556
+
2557
+ ## ServerChallenges
2558
+
2559
+ [ServerChallenges](https://gemarcade.com/developer/api/server#serverchallenges)
2560
+
2561
+ ```ts
2562
+ interface ServerChallenges
2563
+ ```
2564
+
2565
+ The challenges your game config declares, for one player, in one period —
2566
+ with the date, the seed and the stored content your server verifies a
2567
+ result against.
2568
+
2569
+ Reached as `gem.challenges`. The browser's `gem.challenges.list()` answers
2570
+ for the player holding the token; a server holds no player, so this one
2571
+ takes the player's id. Everything else is the same read, and each entry is
2572
+ the same [Challenge](https://gemarcade.com/developer/api/server#challenge).
2573
+
2574
+ ```js
2575
+ export default defineServer((gem) => ({
2576
+ async message(playerId, data, isText) {
2577
+ if (!isText) return;
2578
+ const result = JSON.parse(data);
2579
+ const [now, before] = await Promise.all([
2580
+ gem.challenges.list({ playerId }),
2581
+ gem.challenges.list({ playerId, periodsBack: 1 }),
2582
+ ]);
2583
+ const played = [...now, ...before].find(
2584
+ (c) =>
2585
+ c.id === result.challengeId &&
2586
+ (c.periodLocalDate ?? c.seed) === result.period &&
2587
+ c.dataHash === result.dataHash,
2588
+ );
2589
+ if (!played) return gem.send(playerId, 'rejected');
2590
+ // ...check the result against played.data
2591
+ },
2592
+ }));
2593
+ ```
2594
+
2595
+ ## ServerChallenges.list
2596
+
2597
+ [ServerChallenges.list](https://gemarcade.com/developer/api/server#serverchallenges-list)
2598
+
2599
+ ```ts
2600
+ list(options: ServerListChallengesOptions): Promise<Challenge[]>
2601
+ ```
2602
+
2603
+ Every challenge your config declares, for one player, for one period.
2604
+
2605
+ ⚠️ **Verify against the player's period, never your own clock.** A
2606
+ player's day ends at their own midnight, so the date and the seed you hold
2607
+ for one player are not another player's, and "today" on the machine this
2608
+ runs on is neither. Ask per player, and match on
2609
+ [Challenge.periodLocalDate](https://gemarcade.com/developer/api/server#challenge-periodlocaldate) — or [Challenge.seed](https://gemarcade.com/developer/api/server#challenge-seed) for a
2610
+ `cron` challenge — never on [Challenge.periodStartAt](https://gemarcade.com/developer/api/server#challenge-periodstartat).
2611
+
2612
+ | Parameter | Type |
2613
+ | --- | --- |
2614
+ | `options` | `ServerListChallengesOptions` |
2615
+
2616
+ **Throws.** `TypeError` when `playerId` is missing or empty, or `periodsBack`
2617
+ is not an integer from 0 to 400 — refused before the request.
2618
+
2619
+ `GemApiError` with `reason: 'not_found'` for a player who neither
2620
+ plays your game nor is in it now, and for a channel with no config.
2621
+
2622
+ ## GameApi
2623
+
2624
+ [GameApi](https://gemarcade.com/developer/api/server#gameapi)
2625
+
2626
+ ```ts
2627
+ interface GameApi
2628
+ ```
2629
+
2630
+ What a generated call needs in order to make itself: the client that sends
2631
+ it, and the game it belongs to.
2632
+
2633
+ YOU DO NOT BUILD ONE. `createGemServer` does, and hands you the operations
2634
+ already holding it as `gem.api`. It is named here because it appears in the
2635
+ type of those operations, so a helper of your own that takes them has to be
2636
+ able to spell it.
2637
+
2638
+ `gameId` is a call rather than a field because the game is not known until
2639
+ the platform has issued a credential, which happens after the client exists.
2640
+ It is never something a caller passes: a bundle cannot address another
2641
+ game's data because it never types a game id, in a path or in a query.
2642
+
2643
+ ## GameApi.client
2644
+
2645
+ [GameApi.client](https://gemarcade.com/developer/api/server#gameapi-client)
2646
+
2647
+ ```ts
2648
+ readonly client: GemClient
2649
+ ```
2650
+
2651
+ The client every operation sends through.
2652
+
2653
+ ## GameApi.gameId
2654
+
2655
+ [GameApi.gameId](https://gemarcade.com/developer/api/server#gameapi-gameid)
2656
+
2657
+ ```ts
2658
+ gameId(): string
2659
+ ```
2660
+
2661
+ The game this server is running — taken from its credential.
2662
+
2663
+ ## PlayerText
2664
+
2665
+ [PlayerText](https://gemarcade.com/developer/api/server#playertext)
2666
+
2667
+ ```ts
2668
+ class PlayerText
2669
+ ```
2670
+
2671
+ A player-authored string, which your game must convert before rendering.
2672
+
2673
+ Player names are chosen by players, and length limits are not content limits.
2674
+ If your game assigned one straight to `innerHTML`, whoever picked that name
2675
+ would be running code on your page — with your play token, which is the whole
2676
+ session. So the SDK does not hand you a string: it hands you this, and the
2677
+ unsafe thing does not compile.
2678
+
2679
+ There is exactly one way out, and it belongs at your model boundary:
2680
+
2681
+ ```js
2682
+ import { text } from '@indiegems/gem-web-sdk/sdk';
2683
+
2684
+ const name = text.toPlain(profile.displayName);
2685
+ label.textContent = name;
2686
+ ```
2687
+
2688
+ To sort names, use [text.compare](https://gemarcade.com/developer/api/server#text-compare) rather than converting first — it is
2689
+ locale-aware, which a plain `<` comparison is not.
2690
+
2691
+ ## PlayerText.toString
2692
+
2693
+ [PlayerText.toString](https://gemarcade.com/developer/api/server#playertext-tostring)
2694
+
2695
+ ```ts
2696
+ toString(): string
2697
+ ```
2698
+
2699
+ A sentinel that names its own fix, never the player's text.
2700
+
2701
+ If you see `[gem: use gem.text.toPlain()]` on screen, something rendered
2702
+ the wrapper instead of converting it. That is the gate working — the
2703
+ alternative was shipping an unescaped player name.
2704
+
2705
+ ⚠️ THE SENTINEL NAMES `gem.text`, WHICH DOES NOT EXIST. `text` is imported
2706
+ from the package — `import { text } from '@indiegems/gem-web-sdk/sdk';` —
2707
+ not reached from your `gem` instance. Follow that rather than the message.
2708
+
2709
+ ## PlayerText.toJSON
2710
+
2711
+ [PlayerText.toJSON](https://gemarcade.com/developer/api/server#playertext-tojson)
2712
+
2713
+ ```ts
2714
+ toJSON(): string
2715
+ ```
2716
+
2717
+ The same sentinel, so putting a player name into a save blob or a telemetry
2718
+ payload cannot silently store unescaped text.
2719
+
2720
+ Convert with [text.toPlain](https://gemarcade.com/developer/api/server#text-toplain) before serialising anything you intend to
2721
+ keep.
2722
+
2723
+ ## text
2724
+
2725
+ [text](https://gemarcade.com/developer/api/server#text)
2726
+
2727
+ ```ts
2728
+ text: { toPlain: null; compare: null; equals: null }
2729
+ ```
2730
+
2731
+ The only ways out of [PlayerText](https://gemarcade.com/developer/api/server#playertext).
2732
+
2733
+ Imported from the package rather than reached from `gem`:
2734
+ `import { text } from '@indiegems/gem-web-sdk/sdk';`
2735
+
2736
+ Two calls:
2737
+
2738
+ - `toPlain(value)` — the plain string, safe for `textContent`, canvas
2739
+ `fillText` and interpolation. The one call you make, and it belongs at your
2740
+ model boundary rather than scattered through render code.
2741
+ - `compare(a, b, locale?)` — locale-aware comparison for sorting rosters and
2742
+ leaderboards. Use it instead of converting and comparing with `<`, which
2743
+ sorts by code point and gets accented and non-Latin names wrong. It accepts
2744
+ plain strings too, because real leaderboards mix player names with rows
2745
+ your game authored, and it falls back rather than throwing on a bad locale
2746
+ tag — a throw inside a comparator takes the whole sort down.
2747
+
2748
+ ```js
2749
+ import { text } from '@indiegems/gem-web-sdk/sdk';
2750
+
2751
+ const name = text.toPlain(profile.displayName);
2752
+ players.sort((a, b) => text.compare(a.displayName, b.displayName, locale));
2753
+ ```
2754
+
2755
+ THERE IS DELIBERATELY NO `toHtml`. Escaping depends on where the value
2756
+ lands — safe in element text and quoted attributes, unsafe in URL, style and
2757
+ script contexts — and your framework already escapes. One unambiguous
2758
+ function beats two where one carries a caveat.
2759
+
2760
+ ## text.toPlain
2761
+
2762
+ [text.toPlain](https://gemarcade.com/developer/api/server#text-toplain)
2763
+
2764
+ ```ts
2765
+ toPlain(t: PlayerText): string
2766
+ ```
2767
+
2768
+ The plain string, safe for `textContent`, canvas `fillText` and
2769
+ interpolation.
2770
+
2771
+ The one call you make, and it belongs at your model boundary rather than
2772
+ scattered through your render code:
2773
+
2774
+ ```js
2775
+ const name = text.toPlain(profile.displayName);
2776
+ ```
2777
+
2778
+ Safe for text; NOT pre-escaped for HTML. Do not put the result into
2779
+ `innerHTML`, a URL, a style or a script — convert at the boundary and let
2780
+ your framework escape, which is what it already does.
2781
+
2782
+ | Parameter | Type |
2783
+ | --- | --- |
2784
+ | `t` | `PlayerText` |
2785
+
2786
+ ## text.compare
2787
+
2788
+ [text.compare](https://gemarcade.com/developer/api/server#text-compare)
2789
+
2790
+ ```ts
2791
+ compare(a: string | PlayerText, b: string | PlayerText, locale?: string): number
2792
+ ```
2793
+
2794
+ Locale-aware comparison, for sorting rosters and leaderboards.
2795
+
2796
+ Use this rather than converting to strings and comparing with `<`, which
2797
+ sorts by code point and gets accented and non-Latin names wrong.
2798
+
2799
+ ```js
2800
+ players.sort((a, b) => text.compare(a.displayName, b.displayName, locale));
2801
+ ```
2802
+
2803
+ Accepts plain strings as well as player text, because real leaderboards mix
2804
+ player names with rows your game authored. Pass the player's locale from
2805
+ `GemPrefs.locale`; without one it uses the runtime's. A malformed
2806
+ locale tag falls back rather than throwing — this is a comparator, and a
2807
+ throw here would take the whole sort down with it.
2808
+
2809
+ | Parameter | Type |
2810
+ | --- | --- |
2811
+ | `a` | `string \| PlayerText` |
2812
+ | `b` | `string \| PlayerText` |
2813
+ | `locale` | `string` |
2814
+
2815
+ ## text.equals
2816
+
2817
+ [text.equals](https://gemarcade.com/developer/api/server#text-equals)
2818
+
2819
+ ```ts
2820
+ equals(a: string | PlayerText, b: string | PlayerText): boolean
2821
+ ```
2822
+
2823
+ Whether two names are the same name, without unwrapping either.
2824
+
2825
+ Compares normalised forms, so two spellings of the same name — one
2826
+ composed, one decomposed — come out equal. That matters because a name may
2827
+ reach your game from somewhere other than the arcade.
2828
+
2829
+ **KEY YOUR ROWS ON THE PLAYER'S ID, NEVER ON A NAME.** Names are not
2830
+ identifiers: two players can choose the same one, and a player can change
2831
+ theirs. Use this for display-level comparisons, not for identity.
2832
+
2833
+ | Parameter | Type |
2834
+ | --- | --- |
2835
+ | `a` | `string \| PlayerText` |
2836
+ | `b` | `string \| PlayerText` |
2837
+
2838
+ ## AchievementReceipt.achievementId
2839
+
2840
+ [AchievementReceipt.achievementId](https://gemarcade.com/developer/api/server#achievementreceipt-achievementid)
2841
+
2842
+ ```ts
2843
+ readonly achievementId: string
2844
+ ```
2845
+
2846
+ The achievement's id, as your game config declares it.
2847
+
2848
+ ## AchievementReceipt.progressionDefinitionId
2849
+
2850
+ [AchievementReceipt.progressionDefinitionId](https://gemarcade.com/developer/api/server#achievementreceipt-progressiondefinitionid)
2851
+
2852
+ ```ts
2853
+ readonly progressionDefinitionId: string
2854
+ ```
2855
+
2856
+ The progression the achievement sits on.
2857
+
2858
+ ## AchievementReceipt.points
2859
+
2860
+ [AchievementReceipt.points](https://gemarcade.com/developer/api/server#achievementreceipt-points)
2861
+
2862
+ ```ts
2863
+ readonly points: number
2864
+ ```
2865
+
2866
+ Achievement points the unlock credited. Zero unless `outcome` is
2867
+ `unlocked`, so summing it over receipts counts only what this order
2868
+ credited.
2869
+
2870
+ ## AchievementReceipt.outcome
2871
+
2872
+ [AchievementReceipt.outcome](https://gemarcade.com/developer/api/server#achievementreceipt-outcome)
2873
+
2874
+ ```ts
2875
+ readonly outcome: "unlocked" | "already_unlocked" | "suppressed"
2876
+ ```
2877
+
2878
+ `unlocked`: this order unlocked it. `already_unlocked`: this order crossed
2879
+ the threshold again, after a reset, and the player already holds it.
2880
+ `suppressed`: it was not unlocked, and `reason` says why.
2881
+
2882
+ The set may grow. A receipt with an outcome this SDK does not know is
2883
+ left out rather than reported as one of these, so a `switch` over these
2884
+ three needs no default for correctness — but write one anyway.
2885
+
2886
+ ## AchievementReceipt.reason
2887
+
2888
+ [AchievementReceipt.reason](https://gemarcade.com/developer/api/server#achievementreceipt-reason)
2889
+
2890
+ ```ts
2891
+ readonly reason?: string
2892
+ ```
2893
+
2894
+ Why a `suppressed` achievement was not unlocked — for example
2895
+ `channel=dev`, `principal=developer-minted server token` (a server you
2896
+ started yourself rather than one the platform allocated),
2897
+ `achievements_disabled`, `written_by_config` (something in your config —
2898
+ a recipe, a loot table, a reward — can raise the progression, so a paying
2899
+ achievement on it never unlocks), or `achievements_unavailable`
2900
+ (transient; the next eligible write tries again). For your logs and your
2901
+ developer tools; not text to show a player.
2902
+
2903
+ ## GemServerOptions.baseUrl
2904
+
2905
+ [GemServerOptions.baseUrl](https://gemarcade.com/developer/api/server#gemserveroptions-baseurl)
2906
+
2907
+ ```ts
2908
+ readonly baseUrl?: string
2909
+ ```
2910
+
2911
+ Where to send API calls, INSTEAD of where the platform said.
2912
+
2913
+ The host already names it, and a bundle running for real should not
2914
+ second-guess that. This is for a rig — a tunnel, an API running on your own
2915
+ machine — not for choosing which environment you are in. Setting it in a
2916
+ shipped bundle means your server talks to somewhere the platform did not
2917
+ send it.
2918
+
2919
+ ## GemServerOptions.clock
2920
+
2921
+ [GemServerOptions.clock](https://gemarcade.com/developer/api/server#gemserveroptions-clock)
2922
+
2923
+ ```ts
2924
+ readonly clock?: Clock
2925
+ ```
2926
+
2927
+ Where time comes from. Defaults to the real clock.
2928
+
2929
+ Pass a [Clock](https://gemarcade.com/developer/api/server#clock) in a test so token refresh and retry backoff happen when
2930
+ you say rather than when a real second passes.
2931
+
2932
+ ## GemServerOptions.fetch
2933
+
2934
+ [GemServerOptions.fetch](https://gemarcade.com/developer/api/server#gemserveroptions-fetch)
2935
+
2936
+ ```ts
2937
+ readonly fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
2938
+ ```
2939
+
2940
+ The `fetch` used for every call. Defaults to the runtime's own.
2941
+
2942
+ For tests: a stub here is how you assert what your bundle sends without a
2943
+ network.
2944
+
2945
+ ## GemServerOptions.onAuthStateChange
2946
+
2947
+ [GemServerOptions.onAuthStateChange](https://gemarcade.com/developer/api/server#gemserveroptions-onauthstatechange)
2948
+
2949
+ ```ts
2950
+ readonly onAuthStateChange?: (state: AuthState, reason?: AuthFailure) => void
2951
+ ```
2952
+
2953
+ Called whenever the server's ability to make authenticated calls changes.
2954
+
2955
+ The same signal the browser half delivers to a game, for the same reason:
2956
+ the token is refreshed for you, and this is how you find out when that stops
2957
+ working. See [AuthState](https://gemarcade.com/developer/api/server#authstate) for what to branch on and [AuthFailure](https://gemarcade.com/developer/api/server#authfailure)
2958
+ for the cause.
2959
+
2960
+ ## GemServerIdentity.gameId
2961
+
2962
+ [GemServerIdentity.gameId](https://gemarcade.com/developer/api/server#gemserveridentity-gameid)
2963
+
2964
+ ```ts
2965
+ readonly gameId: string
2966
+ ```
2967
+
2968
+ The game this server belongs to.
2969
+
2970
+ ## GemServerIdentity.channel
2971
+
2972
+ [GemServerIdentity.channel](https://gemarcade.com/developer/api/server#gemserveridentity-channel)
2973
+
2974
+ ```ts
2975
+ readonly channel: string
2976
+ ```
2977
+
2978
+ The channel it was allocated on — `dev`, `test` or `release`.
2979
+
2980
+ ## GemServerIdentity.matchId
2981
+
2982
+ [GemServerIdentity.matchId](https://gemarcade.com/developer/api/server#gemserveridentity-matchid)
2983
+
2984
+ ```ts
2985
+ readonly matchId: string
2986
+ ```
2987
+
2988
+ The match it is running.
2989
+
2990
+ ## GemServerIdentity.gameRoomId
2991
+
2992
+ [GemServerIdentity.gameRoomId](https://gemarcade.com/developer/api/server#gemserveridentity-gameroomid)
2993
+
2994
+ ```ts
2995
+ readonly gameRoomId?: string
2996
+ ```
2997
+
2998
+ The room this match came from, when there is one.
2999
+
3000
+ CHECK IT BEFORE USING IT. It is absent on older platform images, which is
3001
+ also why [GemServer.room](https://gemarcade.com/developer/api/server#gemserver-room) can be absent — the two go together.
3002
+
3003
+ ## GemServer.identity
3004
+
3005
+ [GemServer.identity](https://gemarcade.com/developer/api/server#gemserver-identity)
3006
+
3007
+ ```ts
3008
+ readonly identity: GemServerIdentity
3009
+ ```
3010
+
3011
+ Which game, channel and match this server is running.
3012
+
3013
+ ## GemServer.client
3014
+
3015
+ [GemServer.client](https://gemarcade.com/developer/api/server#gemserver-client)
3016
+
3017
+ ```ts
3018
+ readonly client: GemClient
3019
+ ```
3020
+
3021
+ The HTTP client underneath [GemServer.api](https://gemarcade.com/developer/api/server#gemserver-api), for calls the generated
3022
+ surface does not cover.
3023
+
3024
+ PREFER `api`. It is typed against the platform's description, so a shape
3025
+ that changes becomes a build error in your bundle. This one takes a path and
3026
+ checks nothing — reach for it when you genuinely need something `api` has
3027
+ no method for, and treat that as worth telling us about.
3028
+
3029
+ ## GemServer.api
3030
+
3031
+ [GemServer.api](https://gemarcade.com/developer/api/server#gemserver-api)
3032
+
3033
+ ```ts
3034
+ readonly api: ServerApi
3035
+ ```
3036
+
3037
+ Every platform operation your server can call. This is the one you want.
3038
+
3039
+ See [ServerApi](https://gemarcade.com/developer/api/server#serverapi).
3040
+
3041
+ ## GemServer.match
3042
+
3043
+ [GemServer.match](https://gemarcade.com/developer/api/server#gemserver-match)
3044
+
3045
+ ```ts
3046
+ readonly match: { end: null }
3047
+ ```
3048
+
3049
+ Ending the match.
3050
+
3051
+ `end()` is HOW A SERVER-AUTHORITATIVE GAME FINISHES. It tells the platform
3052
+ the match is over; the platform then tells the runtime, and your server
3053
+ winds down through its ordinary shutdown. You do not stop the process
3054
+ yourself.
3055
+
3056
+ It always means THIS match — the id is already known, so there is nothing to
3057
+ pass and no way to end someone else's.
3058
+
3059
+ The platform half makes the request, not your process: it holds the proof
3060
+ that this server is the one allocated to the match, which the platform
3061
+ requires. On a platform image too old to offer that, `end()` throws rather
3062
+ than sending a request without the proof.
3063
+
3064
+ ## GemServer.match.end
3065
+
3066
+ [GemServer.match.end](https://gemarcade.com/developer/api/server#gemserver-match-end)
3067
+
3068
+ ```ts
3069
+ end(signal?: AbortSignal): Promise<void>
3070
+ ```
3071
+
3072
+ Tell the platform this match is finished.
3073
+
3074
+ | Parameter | Type | Description |
3075
+ | --- | --- | --- |
3076
+ | `signal` | `AbortSignal` | Not used — the platform half bounds its own request. |
3077
+
3078
+ **Throws.** When the platform image running this server predates
3079
+ `host.endMatch`, so the match cannot be ended from here.
3080
+
3081
+ ## GemServer.analytics
3082
+
3083
+ [GemServer.analytics](https://gemarcade.com/developer/api/server#gemserver-analytics)
3084
+
3085
+ ```ts
3086
+ readonly analytics: ServerAnalytics
3087
+ ```
3088
+
3089
+ Recording analytics events about the players in this match.
3090
+
3091
+ THE AUTHORITATIVE HALF of the pair. The browser's `gem.analytics` records
3092
+ game-asserted browser facts, which feed a developer's own numbers and
3093
+ nothing the platform treats as true; a server holds a credential the player
3094
+ does not, so what it records about a run, a score or an outcome is the
3095
+ counterpart a game reaches for when the browser's word is not enough. A
3096
+ game may declare an event `sources: [server]` and shut the client out of it
3097
+ entirely.
3098
+
3099
+ ⚠️ **Every event names its player, and `gem_*` is unavailable here** — see
3100
+ [ServerAnalytics.track](https://gemarcade.com/developer/api/server#serveranalytics-track). And flush before the match ends: there is no
3101
+ page unload to get the tail out on.
3102
+
3103
+ ## GemServer.functions
3104
+
3105
+ [GemServer.functions](https://gemarcade.com/developer/api/server#gemserver-functions)
3106
+
3107
+ ```ts
3108
+ readonly functions: FunctionsApi
3109
+ ```
3110
+
3111
+ Your game's serverless function — `gem.functions.invoke(input)`, the same
3112
+ call a game makes in the browser.
3113
+
3114
+ The call arrives as this server, not as a player: your function's
3115
+ `get-context` says so, and a server's per-minute budget is its own. It runs
3116
+ the function promoted on the channel this match runs on.
3117
+
3118
+ ## GemServer.challenges
3119
+
3120
+ [GemServer.challenges](https://gemarcade.com/developer/api/server#gemserver-challenges)
3121
+
3122
+ ```ts
3123
+ readonly challenges: ServerChallenges
3124
+ ```
3125
+
3126
+ A player's daily challenges — their period, its date, the seed, and any
3127
+ content your studio stored — so your server verifies a result against the
3128
+ board that player was actually given.
3129
+
3130
+ A period is the PLAYER's, not the calendar's: it ends at their own
3131
+ midnight. That is why this takes a player id where the browser's
3132
+ `gem.challenges` takes none. See [ServerChallenges.list](https://gemarcade.com/developer/api/server#serverchallenges-list).
3133
+
3134
+ ## GemServer.room
3135
+
3136
+ [GemServer.room](https://gemarcade.com/developer/api/server#gemserver-room)
3137
+
3138
+ ```ts
3139
+ readonly room?: { get: (options?: { ifNoneMatch?: string; signal?: AbortSignal }) => Promise<VersionedResponse<GameRoomResponseV2>>; args: (options?: { signal?: AbortSignal }) => Promise<RoomArgsResponse> }
3140
+ ```
3141
+
3142
+ Reading the room this match came from — the two calls almost every server
3143
+ makes at startup, already pointed at your own room.
3144
+
3145
+ CHECK IT IS THERE. `room` is absent on older platform images, which is why
3146
+ the type says it might be. `gem.room?.get()` is the shape to write. When it
3147
+ is absent the same reads are still available through `api` if you know the
3148
+ room id from somewhere else.
3149
+
3150
+ ## GemServer.room.get
3151
+
3152
+ [GemServer.room.get](https://gemarcade.com/developer/api/server#gemserver-room-get)
3153
+
3154
+ ```ts
3155
+ get: (options?: { ifNoneMatch?: string; signal?: AbortSignal }) => Promise<VersionedResponse<GameRoomResponseV2>>
3156
+ ```
3157
+
3158
+ The room, in full: the settings it was created with, the template those
3159
+ came from, and every member — with display name, role, ready state and
3160
+ avatar.
3161
+
3162
+ This is what you want at `start()`, and it is a superset of what the older
3163
+ roster push carried.
3164
+
3165
+ ## GemServer.room.args
3166
+
3167
+ [GemServer.room.args](https://gemarcade.com/developer/api/server#gemserver-room-args)
3168
+
3169
+ ```ts
3170
+ args: (options?: { signal?: AbortSignal }) => Promise<RoomArgsResponse>
3171
+ ```
3172
+
3173
+ Just the room's settings.
3174
+
3175
+ A room whose template declares no settings answers 404
3176
+ `room_args_not_defined` rather than an empty object — "this room has no
3177
+ settings" and "this room's settings are empty" are different answers and
3178
+ the API keeps them apart.
3179
+
3180
+ ## GemServer.dispose
3181
+
3182
+ [GemServer.dispose](https://gemarcade.com/developer/api/server#gemserver-dispose)
3183
+
3184
+ ```ts
3185
+ dispose(): void
3186
+ ```
3187
+
3188
+ Stop listening for credential updates.
3189
+
3190
+ You rarely need it: a bundle built with [defineServer](https://gemarcade.com/developer/api/server#defineserver) is disposed for
3191
+ you when the match ends. Call it in a test, or if you built the server
3192
+ yourself and are tearing it down.
3193
+
3194
+ ## ServerTokenGrant.token
3195
+
3196
+ [ServerTokenGrant.token](https://gemarcade.com/developer/api/server#servertokengrant-token)
3197
+
3198
+ ```ts
3199
+ readonly token: string
3200
+ ```
3201
+
3202
+ The credential to present. Never log it and never send it on.
3203
+
3204
+ ## ServerTokenGrant.expiresAt
3205
+
3206
+ [ServerTokenGrant.expiresAt](https://gemarcade.com/developer/api/server#servertokengrant-expiresat)
3207
+
3208
+ ```ts
3209
+ readonly expiresAt: number
3210
+ ```
3211
+
3212
+ When it expires, in seconds since the Unix epoch.
3213
+
3214
+ ## ServerBundleHost.matchId
3215
+
3216
+ [ServerBundleHost.matchId](https://gemarcade.com/developer/api/server#serverbundlehost-matchid)
3217
+
3218
+ ```ts
3219
+ readonly matchId: string
3220
+ ```
3221
+
3222
+ The match this server was allocated for.
3223
+
3224
+ ## ServerBundleHost.gameId
3225
+
3226
+ [ServerBundleHost.gameId](https://gemarcade.com/developer/api/server#serverbundlehost-gameid)
3227
+
3228
+ ```ts
3229
+ readonly gameId: string
3230
+ ```
3231
+
3232
+ The game it belongs to.
3233
+
3234
+ ## ServerBundleHost.channel
3235
+
3236
+ [ServerBundleHost.channel](https://gemarcade.com/developer/api/server#serverbundlehost-channel)
3237
+
3238
+ ```ts
3239
+ readonly channel: string
3240
+ ```
3241
+
3242
+ The channel it was allocated on — `dev`, `test` or `release`.
3243
+
3244
+ ## ServerBundleHost.gameRoomId
3245
+
3246
+ [ServerBundleHost.gameRoomId](https://gemarcade.com/developer/api/server#serverbundlehost-gameroomid)
3247
+
3248
+ ```ts
3249
+ readonly gameRoomId?: string
3250
+ ```
3251
+
3252
+ The room this match came from. MAY BE ABSENT on older platform images, so
3253
+ check before using it — that absence is also why [GemServer.room](https://gemarcade.com/developer/api/server#gemserver-room) can
3254
+ be missing.
3255
+
3256
+ ## ServerBundleHost.apiBaseUrl
3257
+
3258
+ [ServerBundleHost.apiBaseUrl](https://gemarcade.com/developer/api/server#serverbundlehost-apibaseurl)
3259
+
3260
+ ```ts
3261
+ readonly apiBaseUrl?: string
3262
+ ```
3263
+
3264
+ Where to send API calls. Absent on older platform images, which is the case
3265
+ [createGemServer](https://gemarcade.com/developer/api/server#creategemserver) refuses at startup.
3266
+
3267
+ ## ServerBundleHost.serverToken
3268
+
3269
+ [ServerBundleHost.serverToken](https://gemarcade.com/developer/api/server#serverbundlehost-servertoken)
3270
+
3271
+ ```ts
3272
+ serverToken(): ServerTokenGrant | undefined
3273
+ ```
3274
+
3275
+ The credential held right now, if the platform delivered one.
3276
+
3277
+ ## ServerBundleHost.onServerToken
3278
+
3279
+ [ServerBundleHost.onServerToken](https://gemarcade.com/developer/api/server#serverbundlehost-onservertoken)
3280
+
3281
+ ```ts
3282
+ onServerToken(listener: (grant: ServerTokenGrant) => void): () => void
3283
+ ```
3284
+
3285
+ Be told when the credential is replaced.
3286
+
3287
+ Called straight away if one is already held, and again on every refresh.
3288
+ Returns a function that stops the notifications.
3289
+
3290
+ You rarely call this. [createGemServer](https://gemarcade.com/developer/api/server#creategemserver) subscribes for you.
3291
+
3292
+ | Parameter | Type |
3293
+ | --- | --- |
3294
+ | `listener` | `(grant: ServerTokenGrant) => void` |
3295
+
3296
+ ## SendOptions.reliable
3297
+
3298
+ [SendOptions.reliable](https://gemarcade.com/developer/api/server#sendoptions-reliable)
3299
+
3300
+ ```ts
3301
+ readonly reliable?: boolean
3302
+ ```
3303
+
3304
+ Ask for the message to arrive. Defaults to true.
3305
+
3306
+ `false` says you would rather it be dropped than delayed — position updates
3307
+ that a later one supersedes. A transport is free to deliver it reliably
3308
+ anyway; the receiver is still told what you asked for.
3309
+
3310
+ ## SendOptions.ordered
3311
+
3312
+ [SendOptions.ordered](https://gemarcade.com/developer/api/server#sendoptions-ordered)
3313
+
3314
+ ```ts
3315
+ readonly ordered?: boolean
3316
+ ```
3317
+
3318
+ Ask for messages to arrive in the order you sent them. Defaults to true.
3319
+
3320
+ ## BroadcastOptions.reliable
3321
+
3322
+ [BroadcastOptions.reliable](https://gemarcade.com/developer/api/server#broadcastoptions-reliable)
3323
+
3324
+ ```ts
3325
+ readonly reliable?: boolean
3326
+ ```
3327
+
3328
+ Ask for the message to arrive. Defaults to true.
3329
+
3330
+ `false` says you would rather it be dropped than delayed — position updates
3331
+ that a later one supersedes. A transport is free to deliver it reliably
3332
+ anyway; the receiver is still told what you asked for.
3333
+
3334
+ ## BroadcastOptions.ordered
3335
+
3336
+ [BroadcastOptions.ordered](https://gemarcade.com/developer/api/server#broadcastoptions-ordered)
3337
+
3338
+ ```ts
3339
+ readonly ordered?: boolean
3340
+ ```
3341
+
3342
+ Ask for messages to arrive in the order you sent them. Defaults to true.
3343
+
3344
+ ## BroadcastOptions.except
3345
+
3346
+ [BroadcastOptions.except](https://gemarcade.com/developer/api/server#broadcastoptions-except)
3347
+
3348
+ ```ts
3349
+ readonly except?: string
3350
+ ```
3351
+
3352
+ A player to leave out — usually the one whose action caused the broadcast.
3353
+
3354
+ ## GemServerHostProfile.displayName
3355
+
3356
+ [GemServerHostProfile.displayName](https://gemarcade.com/developer/api/server#gemserverhostprofile-displayname)
3357
+
3358
+ ```ts
3359
+ readonly displayName?: string
3360
+ ```
3361
+
3362
+ The player's chosen name, as a plain string.
3363
+
3364
+ PREFER [GemMatchServer.profile](https://gemarcade.com/developer/api/server#gemmatchserver-profile), which gives you the same name as
3365
+ [PlayerText](https://gemarcade.com/developer/api/server#playertext) — a type that reminds you a stranger wrote it.
3366
+
3367
+ ## GemServerHostProfile.displayTag
3368
+
3369
+ [GemServerHostProfile.displayTag](https://gemarcade.com/developer/api/server#gemserverhostprofile-displaytag)
3370
+
3371
+ ```ts
3372
+ readonly displayTag?: string
3373
+ ```
3374
+
3375
+ The precomposed `Name#0001`, as a plain string. It contains the name, so
3376
+ PREFER [GemMatchServer.profile](https://gemarcade.com/developer/api/server#gemmatchserver-profile), which gives it as [PlayerText](https://gemarcade.com/developer/api/server#playertext).
3377
+
3378
+ ## GemServerHostProfile.discriminator
3379
+
3380
+ [GemServerHostProfile.discriminator](https://gemarcade.com/developer/api/server#gemserverhostprofile-discriminator)
3381
+
3382
+ ```ts
3383
+ readonly discriminator?: string
3384
+ ```
3385
+
3386
+ The disambiguating suffix, when the player has one.
3387
+
3388
+ ## GemServerHost.matchId
3389
+
3390
+ [GemServerHost.matchId](https://gemarcade.com/developer/api/server#gemserverhost-matchid)
3391
+
3392
+ ```ts
3393
+ readonly matchId: string
3394
+ ```
3395
+
3396
+ The match this server was allocated for.
3397
+
3398
+ ## GemServerHost.gameId
3399
+
3400
+ [GemServerHost.gameId](https://gemarcade.com/developer/api/server#gemserverhost-gameid)
3401
+
3402
+ ```ts
3403
+ readonly gameId: string
3404
+ ```
3405
+
3406
+ The game it belongs to.
3407
+
3408
+ ## GemServerHost.channel
3409
+
3410
+ [GemServerHost.channel](https://gemarcade.com/developer/api/server#gemserverhost-channel)
3411
+
3412
+ ```ts
3413
+ readonly channel: string
3414
+ ```
3415
+
3416
+ The channel it was allocated on — `dev`, `test` or `release`.
3417
+
3418
+ ## GemServerHost.gameRoomId
3419
+
3420
+ [GemServerHost.gameRoomId](https://gemarcade.com/developer/api/server#gemserverhost-gameroomid)
3421
+
3422
+ ```ts
3423
+ readonly gameRoomId?: string
3424
+ ```
3425
+
3426
+ The room this match came from. MAY BE ABSENT on older platform images, so
3427
+ check before using it — that absence is also why [GemServer.room](https://gemarcade.com/developer/api/server#gemserver-room) can
3428
+ be missing.
3429
+
3430
+ ## GemServerHost.apiBaseUrl
3431
+
3432
+ [GemServerHost.apiBaseUrl](https://gemarcade.com/developer/api/server#gemserverhost-apibaseurl)
3433
+
3434
+ ```ts
3435
+ readonly apiBaseUrl?: string
3436
+ ```
3437
+
3438
+ Where to send API calls. Absent on older platform images, which is the case
3439
+ [createGemServer](https://gemarcade.com/developer/api/server#creategemserver) refuses at startup.
3440
+
3441
+ ## GemServerHost.serverToken
3442
+
3443
+ [GemServerHost.serverToken](https://gemarcade.com/developer/api/server#gemserverhost-servertoken)
3444
+
3445
+ ```ts
3446
+ serverToken(): ServerTokenGrant | undefined
3447
+ ```
3448
+
3449
+ The credential held right now, if the platform delivered one.
3450
+
3451
+ ## GemServerHost.onServerToken
3452
+
3453
+ [GemServerHost.onServerToken](https://gemarcade.com/developer/api/server#gemserverhost-onservertoken)
3454
+
3455
+ ```ts
3456
+ onServerToken(listener: (grant: ServerTokenGrant) => void): () => void
3457
+ ```
3458
+
3459
+ Be told when the credential is replaced.
3460
+
3461
+ Called straight away if one is already held, and again on every refresh.
3462
+ Returns a function that stops the notifications.
3463
+
3464
+ You rarely call this. [createGemServer](https://gemarcade.com/developer/api/server#creategemserver) subscribes for you.
3465
+
3466
+ | Parameter | Type |
3467
+ | --- | --- |
3468
+ | `listener` | `(grant: ServerTokenGrant) => void` |
3469
+
3470
+ ## GemServerHost.sizeClass
3471
+
3472
+ [GemServerHost.sizeClass](https://gemarcade.com/developer/api/server#gemserverhost-sizeclass)
3473
+
3474
+ ```ts
3475
+ readonly sizeClass: string
3476
+ ```
3477
+
3478
+ Which size of machine this server was allocated on.
3479
+
3480
+ ## GemServerHost.roomArgs
3481
+
3482
+ [GemServerHost.roomArgs](https://gemarcade.com/developer/api/server#gemserverhost-roomargs)
3483
+
3484
+ ```ts
3485
+ readonly roomArgs?: Readonly<Record<string, RoomArgValue>>
3486
+ ```
3487
+
3488
+ **Deprecated.** Read `gem.room?.args()` instead — it gets the same settings from
3489
+ the platform and distinguishes "no settings are declared" from "they are
3490
+ declared and empty".
3491
+
3492
+ Still delivered today. The member keeps compiling for the rest of `3.x`,
3493
+ and `4.0.0` is the earliest release that may take it off the type. Whether
3494
+ a server keeps sending a value is a separate question, decided where the
3495
+ dedicated-server image is built rather than by a package release — so check
3496
+ for the room read and keep this as the fallback. Both halves are in
3497
+ [the deprecation policy](https://gemarcade.com/developer/docs/deprecation-policy).
3498
+
3499
+ The room's settings as they were at match start.
3500
+
3501
+ `undefined` covers two cases you cannot tell apart and do not need to: the
3502
+ room type declares no settings, or the platform image is older than this
3503
+ member.
3504
+
3505
+ ## GemServerHost.roster
3506
+
3507
+ [GemServerHost.roster](https://gemarcade.com/developer/api/server#gemserverhost-roster)
3508
+
3509
+ ```ts
3510
+ readonly roster: readonly string[]
3511
+ ```
3512
+
3513
+ **Deprecated.** Read `gem.room?.get()` instead — live membership, with names,
3514
+ roles and ready state.
3515
+
3516
+ Still delivered today. The member keeps compiling for the rest of `3.x`,
3517
+ and `4.0.0` is the earliest release that may take it off the type. Whether
3518
+ a server keeps sending a value is a separate question, decided where the
3519
+ dedicated-server image is built rather than by a package release — so check
3520
+ for the room read and keep this as the fallback. Both halves are in
3521
+ [the deprecation policy](https://gemarcade.com/developer/docs/deprecation-policy).
3522
+
3523
+ Who was in the room when this server was allocated.
3524
+
3525
+ A SIZING HINT, NOT A GUEST LIST. It is never revised, and it does not decide
3526
+ who may connect — the platform admits players by credential. A player who
3527
+ joined after allocation holds a valid one and is not in here.
3528
+
3529
+ ## GemServerHost.players
3530
+
3531
+ [GemServerHost.players](https://gemarcade.com/developer/api/server#gemserverhost-players)
3532
+
3533
+ ```ts
3534
+ players(): string[]
3535
+ ```
3536
+
3537
+ The player ids connected right now, as a fresh array each call.
3538
+
3539
+ ## GemServerHost.profile
3540
+
3541
+ [GemServerHost.profile](https://gemarcade.com/developer/api/server#gemserverhost-profile)
3542
+
3543
+ ```ts
3544
+ profile(playerId: string): GemServerHostProfile | undefined
3545
+ ```
3546
+
3547
+ A player's display identity, if the platform resolved one.
3548
+
3549
+ OPTIONAL TWICE OVER: the method itself may be absent on an older platform
3550
+ image, and it may answer `undefined` for a player it has nothing for.
3551
+ [GemMatchServer.profile](https://gemarcade.com/developer/api/server#gemmatchserver-profile) collapses both into one `null`.
3552
+
3553
+ | Parameter | Type |
3554
+ | --- | --- |
3555
+ | `playerId` | `string` |
3556
+
3557
+ ## GemServerHost.playerNumber
3558
+
3559
+ [GemServerHost.playerNumber](https://gemarcade.com/developer/api/server#gemserverhost-playernumber)
3560
+
3561
+ ```ts
3562
+ playerNumber(playerId: string): number | undefined
3563
+ ```
3564
+
3565
+ The 1-based seat the platform gave a connected player.
3566
+
3567
+ May be absent as a method on an older platform image, and answers
3568
+ `undefined` for an id it has not seated.
3569
+
3570
+ | Parameter | Type |
3571
+ | --- | --- |
3572
+ | `playerId` | `string` |
3573
+
3574
+ ## GemServerHost.send
3575
+
3576
+ [GemServerHost.send](https://gemarcade.com/developer/api/server#gemserverhost-send)
3577
+
3578
+ ```ts
3579
+ send(playerId: string, data: string | Uint8Array<ArrayBufferLike>, options?: SendOptions): void
3580
+ ```
3581
+
3582
+ Send to one player. A string goes as text; bytes go as binary, and the
3583
+ receiver is told which.
3584
+
3585
+ | Parameter | Type | Description |
3586
+ | --- | --- | --- |
3587
+ | `playerId` | `string` | |
3588
+ | `data` | `string \| Uint8Array<ArrayBufferLike>` | |
3589
+ | `options` | `SendOptions` | What delivery you are asking for — see [SendOptions](https://gemarcade.com/developer/api/server#sendoptions).
3590
+ Pass them freely: an older platform image ignores them harmlessly. |
3591
+
3592
+ ## GemServerHost.broadcast
3593
+
3594
+ [GemServerHost.broadcast](https://gemarcade.com/developer/api/server#gemserverhost-broadcast)
3595
+
3596
+ ```ts
3597
+ broadcast(data: string | Uint8Array<ArrayBufferLike>, options?: BroadcastOptions): void
3598
+ ```
3599
+
3600
+ Send to every connected player. See [BroadcastOptions](https://gemarcade.com/developer/api/server#broadcastoptions) for how, and
3601
+ for leaving one out.
3602
+
3603
+ | Parameter | Type |
3604
+ | --- | --- |
3605
+ | `data` | `string \| Uint8Array<ArrayBufferLike>` |
3606
+ | `options` | `BroadcastOptions` |
3607
+
3608
+ ## GemServerHost.kick
3609
+
3610
+ [GemServerHost.kick](https://gemarcade.com/developer/api/server#gemserverhost-kick)
3611
+
3612
+ ```ts
3613
+ kick(playerId: string, reason?: string): void
3614
+ ```
3615
+
3616
+ Disconnect a player. `reason` is shown to them and recorded in the logs.
3617
+
3618
+ | Parameter | Type |
3619
+ | --- | --- |
3620
+ | `playerId` | `string` |
3621
+ | `reason` | `string` |
3622
+
3623
+ ## GemServerHost.endMatch
3624
+
3625
+ [GemServerHost.endMatch](https://gemarcade.com/developer/api/server#gemserverhost-endmatch)
3626
+
3627
+ ```ts
3628
+ endMatch(): Promise<"ok" | "already_ended" | "refused" | "failed" | string & object>
3629
+ ```
3630
+
3631
+ Ask the platform half to end this match, with the allocation's own proof.
3632
+
3633
+ MAY BE ABSENT on older platform images. `gem.match.end()` is this call —
3634
+ the request carries the allocation nonce, which never enters the bundle
3635
+ process — and refuses on an image that does not offer it. You rarely call
3636
+ this yourself.
3637
+
3638
+ Resolves with what the platform half did: `ok`, `already_ended` (this
3639
+ match was already over, or you asked twice), `refused` (the allocation was
3640
+ not ready yet) or `failed` (the platform could not be reached).
3641
+
3642
+ ## GemServerHost.log
3643
+
3644
+ [GemServerHost.log](https://gemarcade.com/developer/api/server#gemserverhost-log)
3645
+
3646
+ ```ts
3647
+ log(message: string): void
3648
+ ```
3649
+
3650
+ Write a line to the match's logs.
3651
+
3652
+ THE ONLY ROUTE. `console.log` writes to standard output, which is how the
3653
+ runtime talks to your server — a line written there arrives as a complaint
3654
+ about your bundle rather than as a log.
3655
+
3656
+ | Parameter | Type |
3657
+ | --- | --- |
3658
+ | `message` | `string` |
3659
+
3660
+ ## GemServerHost.warn
3661
+
3662
+ [GemServerHost.warn](https://gemarcade.com/developer/api/server#gemserverhost-warn)
3663
+
3664
+ ```ts
3665
+ warn(message: string): void
3666
+ ```
3667
+
3668
+ Like [GemServerHost.log](https://gemarcade.com/developer/api/server#gemserverhost-log), marked as a warning.
3669
+
3670
+ | Parameter | Type |
3671
+ | --- | --- |
3672
+ | `message` | `string` |
3673
+
3674
+ ## HostTokenSourceOptions.clock
3675
+
3676
+ [HostTokenSourceOptions.clock](https://gemarcade.com/developer/api/server#hosttokensourceoptions-clock)
3677
+
3678
+ ```ts
3679
+ readonly clock: Clock
3680
+ ```
3681
+
3682
+ Where time comes from — used for expiry and for the wait below. Pass a
3683
+ [Clock](https://gemarcade.com/developer/api/server#clock) in a test.
3684
+
3685
+ ## HostTokenSourceOptions.timeoutMs
3686
+
3687
+ [HostTokenSourceOptions.timeoutMs](https://gemarcade.com/developer/api/server#hosttokensourceoptions-timeoutms)
3688
+
3689
+ ```ts
3690
+ readonly timeoutMs?: number
3691
+ ```
3692
+
3693
+ How long to wait for the platform to deliver a credential before giving up
3694
+ on a call. Defaults to 10 seconds.
3695
+
3696
+ ## HostTokenSourceOptions.onStateChange
3697
+
3698
+ [HostTokenSourceOptions.onStateChange](https://gemarcade.com/developer/api/server#hosttokensourceoptions-onstatechange)
3699
+
3700
+ ```ts
3701
+ readonly onStateChange?: (state: AuthState, reason?: AuthFailure) => void
3702
+ ```
3703
+
3704
+ Called when the ability to make authenticated calls changes. See
3705
+ [AuthState](https://gemarcade.com/developer/api/server#authstate) for what to branch on.
3706
+
3707
+ ## HostTokenSource.dispose
3708
+
3709
+ [HostTokenSource.dispose](https://gemarcade.com/developer/api/server#hosttokensource-dispose)
3710
+
3711
+ ```ts
3712
+ dispose(): void
3713
+ ```
3714
+
3715
+ Stop refreshing the credential.
3716
+
3717
+ CALL IT AT SHUTDOWN. A pending refresh timer keeps the process alive, so a
3718
+ bundle that skips this looks like it has hung rather than exited.
3719
+
3720
+ ## GemServerBundle.init
3721
+
3722
+ [GemServerBundle.init](https://gemarcade.com/developer/api/server#gemserverbundle-init)
3723
+
3724
+ ```ts
3725
+ init(): void | Promise<void>
3726
+ ```
3727
+
3728
+ Once, before any player arrives. Set your world up here.
3729
+
3730
+ YOU HAVE 60 SECONDS. Throwing fails the match, so do not start anything here
3731
+ you are not prepared to have fail loudly.
3732
+
3733
+ ## GemServerBundle.join
3734
+
3735
+ [GemServerBundle.join](https://gemarcade.com/developer/api/server#gemserverbundle-join)
3736
+
3737
+ ```ts
3738
+ join(playerId: string, meta?: { playerNumber?: number; transport?: string; rejoin?: boolean; superseded?: boolean }): void | Promise<void>
3739
+ ```
3740
+
3741
+ A player connected, and the platform has already verified who they are.
3742
+
3743
+ They are ALREADY in `host.players()` by the time this runs.
3744
+
3745
+ `meta.rejoin` marks a player the platform has already seen in this match,
3746
+ as against a first arrival — the cue to restore what you kept for them
3747
+ instead of seating them anew. `meta.playerNumber` is the number they
3748
+ already had, so state you keyed by it still applies.
3749
+
3750
+ Read `rejoin` as a FLOOR, not a decision procedure. It is absent, not
3751
+ `false`, when unset, and an older platform image sets it only for the
3752
+ narrow case where the player's previous connection was still open when the
3753
+ new one landed — a reconnect it had already reaped arrives unmarked there.
3754
+ So an absent `rejoin` means "not known to be a reconnect", never
3755
+ "certainly a new player", and `playerNumber` is what to key state by
3756
+ either way.
3757
+
3758
+ `meta.superseded` narrows a rejoin further: the player's EARLIER
3759
+ connection was still attached when this one landed, so they never really
3760
+ left — the `leave` you heard a moment ago (its `cause` is `superseded`)
3761
+ was synthetic, and the pair is one reconnect rather than a departure and
3762
+ a return. Without it a bundle could not tell that pair from a genuine
3763
+ drop-and-rejoin, and so could not decide whether to hold a team, a slot
3764
+ or a score for a player. Present on the
3765
+ dedicated runtime and on a listen host alike; absent, not `false`, when
3766
+ nothing was superseded, and on platform images that predate it.
3767
+
3768
+ | Parameter | Type | Description |
3769
+ | --- | --- | --- |
3770
+ | `playerId` | `string` | Who joined. Take ids from here rather than from anything a
3771
+ client sent you. |
3772
+ | `meta` | `{ playerNumber?: number; transport?: string; rejoin?: boolean; superseded?: boolean }` | What the platform knows about the arrival — the seat it gave
3773
+ them, which transport carried them, whether this is a player coming
3774
+ back, and whether they replaced a connection that was still up. MAY BE
3775
+ ABSENT on older platform images, so default before use. |
3776
+
3777
+ ## GemServerBundle.leave
3778
+
3779
+ [GemServerBundle.leave](https://gemarcade.com/developer/api/server#gemserverbundle-leave)
3780
+
3781
+ ```ts
3782
+ leave(playerId: string, meta?: { cause?: string }): void | Promise<void>
3783
+ ```
3784
+
3785
+ A player's connection closed. They are ALREADY gone from `host.players()`.
3786
+
3787
+ A player whose link dropped and who is reconnecting arrives here first and
3788
+ in `join` again, with `meta.rejoin` set on platform images that report it.
3789
+ Nothing is replayed between the two.
3790
+
3791
+ Two shapes reach here, and `meta.cause` tells them apart. A redial that
3792
+ lands while the dead connection is still attached supersedes it: the cause
3793
+ is `superseded`, this `leave` is synthetic, and the `join` follows
3794
+ immediately. A connection the platform gave up on first ends with its own
3795
+ cause, and the player's redial may be seconds behind or may never come.
3796
+ Either way the `join`, if it comes, is the same player and carries the
3797
+ same `playerNumber` — so do not tear down what you are holding for them
3798
+ on the strength of the cause alone.
3799
+
3800
+ | Parameter | Type | Description |
3801
+ | --- | --- | --- |
3802
+ | `playerId` | `string` | Who left. |
3803
+ | `meta` | `{ cause?: string }` | Why, when the platform knows. May be absent. |
3804
+
3805
+ ## GemServerBundle.message
3806
+
3807
+ [GemServerBundle.message](https://gemarcade.com/developer/api/server#gemserverbundle-message)
3808
+
3809
+ ```ts
3810
+ message(playerId: string, data: string | Uint8Array<ArrayBufferLike>, isText: boolean, meta?: { reliable?: boolean; ordered?: boolean; sessionId?: number; playerNumber?: number }): void | Promise<void>
3811
+ ```
3812
+
3813
+ A player sent you something.
3814
+
3815
+ | Parameter | Type | Description |
3816
+ | --- | --- | --- |
3817
+ | `playerId` | `string` | Who sent it. |
3818
+ | `data` | `string \| Uint8Array<ArrayBufferLike>` | The message: a string when `isText`, bytes otherwise. |
3819
+ | `isText` | `boolean` | Which of the two `data` is. CHECK IT rather than guessing —
3820
+ treating bytes as text corrupts them. |
3821
+ | `meta` | `{ reliable?: boolean; ordered?: boolean; sessionId?: number; playerNumber?: number }` | What the sender ASKED FOR — `reliable` and `ordered` — carried
3822
+ across whatever the transport actually did. May be absent. |
3823
+
3824
+ ## GemServerBundle.shutdown
3825
+
3826
+ [GemServerBundle.shutdown](https://gemarcade.com/developer/api/server#gemserverbundle-shutdown)
3827
+
3828
+ ```ts
3829
+ shutdown(info?: ServerShutdownInfo): void | Promise<void>
3830
+ ```
3831
+
3832
+ The match is ending, or the platform is reclaiming the machine. Save what
3833
+ just happened.
3834
+
3835
+ YOU HAVE LONGER THAN YOU THINK, and how much depends on which of the two it
3836
+ is. When the match ends: 10 minutes before the process goes — deliberately
3837
+ generous, because writing results is what servers do here. When the platform
3838
+ is reclaiming the machine: up to 2 hours, so a match with players still in it
3839
+ plays out rather than dying mid-game.
3840
+
3841
+ `info.reason` says which, on platform images that report it — see
3842
+ [ServerShutdownInfo](https://gemarcade.com/developer/api/server#servershutdowninfo). On a reclaim (`signal`) do NOT end the match:
3843
+ the players are still in it, and ending it here drops every one of them.
3844
+
3845
+ ```js
3846
+ async shutdown(info) {
3847
+ await saveResults();
3848
+ if (info?.reason !== 'signal') await gem.match.end();
3849
+ }
3850
+ ```
3851
+
3852
+ | Parameter | Type | Description |
3853
+ | --- | --- | --- |
3854
+ | `info` | `ServerShutdownInfo` | Why, when the platform says. ABSENT on older platform images,
3855
+ so default before use. |
3856
+
3857
+ **Guides.** [server-bundles](https://gemarcade.com/developer/docs/server-bundles#how-long-your-server-lives)
3858
+
3859
+ ## ServerShutdownInfo.reason
3860
+
3861
+ [ServerShutdownInfo.reason](https://gemarcade.com/developer/api/server#servershutdowninfo-reason)
3862
+
3863
+ ```ts
3864
+ readonly reason: string & object | "match_ended" | "idle_timeout" | "signal"
3865
+ ```
3866
+
3867
+ Which of the above.
3868
+
3869
+ ## ServerShutdownInfo.endedReason
3870
+
3871
+ [ServerShutdownInfo.endedReason](https://gemarcade.com/developer/api/server#servershutdowninfo-endedreason)
3872
+
3873
+ ```ts
3874
+ readonly endedReason?: string
3875
+ ```
3876
+
3877
+ On `match_ended`: why, in the client's `matchEnded` vocabulary (`explicit`,
3878
+ `expired`, `all_left`, …), or `unknown` when the runtime passed a word this
3879
+ SDK does not know. Absent when the runtime passed nothing.
3880
+
3881
+ ## GemServerLifecycle.init
3882
+
3883
+ [GemServerLifecycle.init](https://gemarcade.com/developer/api/server#gemserverlifecycle-init)
3884
+
3885
+ ```ts
3886
+ init(host: GemServerHost): Promise<void>
3887
+ ```
3888
+
3889
+ Called first, handing over the host object. This is where the SDK takes it
3890
+ and where your own `init` runs.
3891
+
3892
+ | Parameter | Type |
3893
+ | --- | --- |
3894
+ | `host` | `GemServerHost` |
3895
+
3896
+ ## GemServerLifecycle.join
3897
+
3898
+ [GemServerLifecycle.join](https://gemarcade.com/developer/api/server#gemserverlifecycle-join)
3899
+
3900
+ ```ts
3901
+ join(playerId: string, meta?: { playerNumber?: number; transport?: string; rejoin?: boolean; superseded?: boolean }): void | Promise<void>
3902
+ ```
3903
+
3904
+ Forwarded from your bundle, when you defined a `join`.
3905
+
3906
+ | Parameter | Type |
3907
+ | --- | --- |
3908
+ | `playerId` | `string` |
3909
+ | `meta` | `{ playerNumber?: number; transport?: string; rejoin?: boolean; superseded?: boolean }` |
3910
+
3911
+ ## GemServerLifecycle.leave
3912
+
3913
+ [GemServerLifecycle.leave](https://gemarcade.com/developer/api/server#gemserverlifecycle-leave)
3914
+
3915
+ ```ts
3916
+ leave(playerId: string, meta?: { cause?: string }): void | Promise<void>
3917
+ ```
3918
+
3919
+ Forwarded from your bundle, when you defined a `leave`.
3920
+
3921
+ | Parameter | Type |
3922
+ | --- | --- |
3923
+ | `playerId` | `string` |
3924
+ | `meta` | `{ cause?: string }` |
3925
+
3926
+ ## GemServerLifecycle.message
3927
+
3928
+ [GemServerLifecycle.message](https://gemarcade.com/developer/api/server#gemserverlifecycle-message)
3929
+
3930
+ ```ts
3931
+ message(playerId: string, data: string | Uint8Array<ArrayBufferLike>, isText: boolean, meta?: { reliable?: boolean; ordered?: boolean; sessionId?: number; playerNumber?: number }): void | Promise<void>
3932
+ ```
3933
+
3934
+ Forwarded from your bundle, when you defined a `message`.
3935
+
3936
+ | Parameter | Type |
3937
+ | --- | --- |
3938
+ | `playerId` | `string` |
3939
+ | `data` | `string \| Uint8Array<ArrayBufferLike>` |
3940
+ | `isText` | `boolean` |
3941
+ | `meta` | `{ reliable?: boolean; ordered?: boolean; sessionId?: number; playerNumber?: number }` |
3942
+
3943
+ ## GemServerLifecycle.shutdown
3944
+
3945
+ [GemServerLifecycle.shutdown](https://gemarcade.com/developer/api/server#gemserverlifecycle-shutdown)
3946
+
3947
+ ```ts
3948
+ shutdown(info?: RuntimeShutdownPayload): Promise<void>
3949
+ ```
3950
+
3951
+ Always defined, even if your bundle has no `shutdown` — the SDK has teardown
3952
+ of its own to do, and it runs whether or not yours threw.
3953
+
3954
+ | Parameter | Type | Description |
3955
+ | --- | --- | --- |
3956
+ | `info` | `RuntimeShutdownPayload` | What the runtime passed, when it passed anything: the raw
3957
+ `{ reason, platform_reason }` of its shutdown frame, which the SDK
3958
+ narrows into [ServerShutdownInfo](https://gemarcade.com/developer/api/server#servershutdowninfo) for your bundle. |
3959
+
3960
+ ## RuntimeShutdownPayload.reason
3961
+
3962
+ [RuntimeShutdownPayload.reason](https://gemarcade.com/developer/api/server#runtimeshutdownpayload-reason)
3963
+
3964
+ ```ts
3965
+ readonly reason?: string
3966
+ ```
3967
+
3968
+ The runtime's cause: `match_ended`, `idle_timeout`, `signal`.
3969
+
3970
+ ## RuntimeShutdownPayload.platform_reason
3971
+
3972
+ [RuntimeShutdownPayload.platform_reason](https://gemarcade.com/developer/api/server#runtimeshutdownpayload-platform_reason)
3973
+
3974
+ ```ts
3975
+ readonly platform_reason?: string
3976
+ ```
3977
+
3978
+ On `match_ended`, the platform's reason as the runtime's frame spells it.
3979
+
3980
+ ## RuntimeShutdownPayload.endedReason
3981
+
3982
+ [RuntimeShutdownPayload.endedReason](https://gemarcade.com/developer/api/server#runtimeshutdownpayload-endedreason)
3983
+
3984
+ ```ts
3985
+ readonly endedReason?: string
3986
+ ```
3987
+
3988
+ The same, as the SDK spells it — for a lifecycle driven by hand.
3989
+
3990
+ ## GemServerProfile.displayName
3991
+
3992
+ [GemServerProfile.displayName](https://gemarcade.com/developer/api/server#gemserverprofile-displayname)
3993
+
3994
+ ```ts
3995
+ readonly displayName: PlayerText
3996
+ ```
3997
+
3998
+ The name the player chose — so it is [PlayerText](https://gemarcade.com/developer/api/server#playertext), not a plain string.
3999
+
4000
+ The type is the reminder: this is text a stranger wrote. `text.toPlain` is
4001
+ the one way to turn it into a string, and on a server the honest answer is
4002
+ usually that you never need to.
4003
+
4004
+ ## GemServerProfile.displayTag
4005
+
4006
+ [GemServerProfile.displayTag](https://gemarcade.com/developer/api/server#gemserverprofile-displaytag)
4007
+
4008
+ ```ts
4009
+ readonly displayTag?: PlayerText
4010
+ ```
4011
+
4012
+ The precomposed `Name#0001`, when the platform has one. It contains the
4013
+ name, so it is [PlayerText](https://gemarcade.com/developer/api/server#playertext) too — the same type every other surface
4014
+ that shows a player gives it.
4015
+
4016
+ ## GemServerProfile.discriminator
4017
+
4018
+ [GemServerProfile.discriminator](https://gemarcade.com/developer/api/server#gemserverprofile-discriminator)
4019
+
4020
+ ```ts
4021
+ readonly discriminator?: string
4022
+ ```
4023
+
4024
+ The disambiguating suffix, when the player has one.
4025
+
4026
+ ## GemMatchServer.identity
4027
+
4028
+ [GemMatchServer.identity](https://gemarcade.com/developer/api/server#gemmatchserver-identity)
4029
+
4030
+ ```ts
4031
+ readonly identity: GemServerIdentity
4032
+ ```
4033
+
4034
+ Which game, channel and match this server is running.
4035
+
4036
+ ## GemMatchServer.client
4037
+
4038
+ [GemMatchServer.client](https://gemarcade.com/developer/api/server#gemmatchserver-client)
4039
+
4040
+ ```ts
4041
+ readonly client: GemClient
4042
+ ```
4043
+
4044
+ The HTTP client underneath [GemServer.api](https://gemarcade.com/developer/api/server#gemserver-api), for calls the generated
4045
+ surface does not cover.
4046
+
4047
+ PREFER `api`. It is typed against the platform's description, so a shape
4048
+ that changes becomes a build error in your bundle. This one takes a path and
4049
+ checks nothing — reach for it when you genuinely need something `api` has
4050
+ no method for, and treat that as worth telling us about.
4051
+
4052
+ ## GemMatchServer.api
4053
+
4054
+ [GemMatchServer.api](https://gemarcade.com/developer/api/server#gemmatchserver-api)
4055
+
4056
+ ```ts
4057
+ readonly api: ServerApi
4058
+ ```
4059
+
4060
+ Every platform operation your server can call. This is the one you want.
4061
+
4062
+ See [ServerApi](https://gemarcade.com/developer/api/server#serverapi).
4063
+
4064
+ ## GemMatchServer.match
4065
+
4066
+ [GemMatchServer.match](https://gemarcade.com/developer/api/server#gemmatchserver-match)
4067
+
4068
+ ```ts
4069
+ readonly match: { end: null }
4070
+ ```
4071
+
4072
+ Ending the match.
4073
+
4074
+ `end()` is HOW A SERVER-AUTHORITATIVE GAME FINISHES. It tells the platform
4075
+ the match is over; the platform then tells the runtime, and your server
4076
+ winds down through its ordinary shutdown. You do not stop the process
4077
+ yourself.
4078
+
4079
+ It always means THIS match — the id is already known, so there is nothing to
4080
+ pass and no way to end someone else's.
4081
+
4082
+ The platform half makes the request, not your process: it holds the proof
4083
+ that this server is the one allocated to the match, which the platform
4084
+ requires. On a platform image too old to offer that, `end()` throws rather
4085
+ than sending a request without the proof.
4086
+
4087
+ ## GemMatchServer.match.end
4088
+
4089
+ [GemMatchServer.match.end](https://gemarcade.com/developer/api/server#gemmatchserver-match-end)
4090
+
4091
+ ```ts
4092
+ end(signal?: AbortSignal): Promise<void>
4093
+ ```
4094
+
4095
+ Tell the platform this match is finished.
4096
+
4097
+ | Parameter | Type | Description |
4098
+ | --- | --- | --- |
4099
+ | `signal` | `AbortSignal` | Not used — the platform half bounds its own request. |
4100
+
4101
+ **Throws.** When the platform image running this server predates
4102
+ `host.endMatch`, so the match cannot be ended from here.
4103
+
4104
+ ## GemMatchServer.analytics
4105
+
4106
+ [GemMatchServer.analytics](https://gemarcade.com/developer/api/server#gemmatchserver-analytics)
4107
+
4108
+ ```ts
4109
+ readonly analytics: ServerAnalytics
4110
+ ```
4111
+
4112
+ Recording analytics events about the players in this match.
4113
+
4114
+ THE AUTHORITATIVE HALF of the pair. The browser's `gem.analytics` records
4115
+ game-asserted browser facts, which feed a developer's own numbers and
4116
+ nothing the platform treats as true; a server holds a credential the player
4117
+ does not, so what it records about a run, a score or an outcome is the
4118
+ counterpart a game reaches for when the browser's word is not enough. A
4119
+ game may declare an event `sources: [server]` and shut the client out of it
4120
+ entirely.
4121
+
4122
+ ⚠️ **Every event names its player, and `gem_*` is unavailable here** — see
4123
+ [ServerAnalytics.track](https://gemarcade.com/developer/api/server#serveranalytics-track). And flush before the match ends: there is no
4124
+ page unload to get the tail out on.
4125
+
4126
+ ## GemMatchServer.functions
4127
+
4128
+ [GemMatchServer.functions](https://gemarcade.com/developer/api/server#gemmatchserver-functions)
4129
+
4130
+ ```ts
4131
+ readonly functions: FunctionsApi
4132
+ ```
4133
+
4134
+ Your game's serverless function — `gem.functions.invoke(input)`, the same
4135
+ call a game makes in the browser.
4136
+
4137
+ The call arrives as this server, not as a player: your function's
4138
+ `get-context` says so, and a server's per-minute budget is its own. It runs
4139
+ the function promoted on the channel this match runs on.
4140
+
4141
+ ## GemMatchServer.challenges
4142
+
4143
+ [GemMatchServer.challenges](https://gemarcade.com/developer/api/server#gemmatchserver-challenges)
4144
+
4145
+ ```ts
4146
+ readonly challenges: ServerChallenges
4147
+ ```
4148
+
4149
+ A player's daily challenges — their period, its date, the seed, and any
4150
+ content your studio stored — so your server verifies a result against the
4151
+ board that player was actually given.
4152
+
4153
+ A period is the PLAYER's, not the calendar's: it ends at their own
4154
+ midnight. That is why this takes a player id where the browser's
4155
+ `gem.challenges` takes none. See [ServerChallenges.list](https://gemarcade.com/developer/api/server#serverchallenges-list).
4156
+
4157
+ ## GemMatchServer.room
4158
+
4159
+ [GemMatchServer.room](https://gemarcade.com/developer/api/server#gemmatchserver-room)
4160
+
4161
+ ```ts
4162
+ readonly room?: { get: (options?: { ifNoneMatch?: string; signal?: AbortSignal }) => Promise<VersionedResponse<GameRoomResponseV2>>; args: (options?: { signal?: AbortSignal }) => Promise<RoomArgsResponse> }
4163
+ ```
4164
+
4165
+ Reading the room this match came from — the two calls almost every server
4166
+ makes at startup, already pointed at your own room.
4167
+
4168
+ CHECK IT IS THERE. `room` is absent on older platform images, which is why
4169
+ the type says it might be. `gem.room?.get()` is the shape to write. When it
4170
+ is absent the same reads are still available through `api` if you know the
4171
+ room id from somewhere else.
4172
+
4173
+ ## GemMatchServer.room.get
4174
+
4175
+ [GemMatchServer.room.get](https://gemarcade.com/developer/api/server#gemmatchserver-room-get)
4176
+
4177
+ ```ts
4178
+ get: (options?: { ifNoneMatch?: string; signal?: AbortSignal }) => Promise<VersionedResponse<GameRoomResponseV2>>
4179
+ ```
4180
+
4181
+ The room, in full: the settings it was created with, the template those
4182
+ came from, and every member — with display name, role, ready state and
4183
+ avatar.
4184
+
4185
+ This is what you want at `start()`, and it is a superset of what the older
4186
+ roster push carried.
4187
+
4188
+ ## GemMatchServer.room.args
4189
+
4190
+ [GemMatchServer.room.args](https://gemarcade.com/developer/api/server#gemmatchserver-room-args)
4191
+
4192
+ ```ts
4193
+ args: (options?: { signal?: AbortSignal }) => Promise<RoomArgsResponse>
4194
+ ```
4195
+
4196
+ Just the room's settings.
4197
+
4198
+ A room whose template declares no settings answers 404
4199
+ `room_args_not_defined` rather than an empty object — "this room has no
4200
+ settings" and "this room's settings are empty" are different answers and
4201
+ the API keeps them apart.
4202
+
4203
+ ## GemMatchServer.dispose
4204
+
4205
+ [GemMatchServer.dispose](https://gemarcade.com/developer/api/server#gemmatchserver-dispose)
4206
+
4207
+ ```ts
4208
+ dispose(): void
4209
+ ```
4210
+
4211
+ Stop listening for credential updates.
4212
+
4213
+ You rarely need it: a bundle built with [defineServer](https://gemarcade.com/developer/api/server#defineserver) is disposed for
4214
+ you when the match ends. Call it in a test, or if you built the server
4215
+ yourself and are tearing it down.
4216
+
4217
+ ## GemMatchServer.sizeClass
4218
+
4219
+ [GemMatchServer.sizeClass](https://gemarcade.com/developer/api/server#gemmatchserver-sizeclass)
4220
+
4221
+ ```ts
4222
+ readonly sizeClass: string
4223
+ ```
4224
+
4225
+ Which size of machine this server was allocated on.
4226
+
4227
+ Useful if your game scales what it does to the hardware it got.
4228
+
4229
+ ## GemMatchServer.roomArgs
4230
+
4231
+ [GemMatchServer.roomArgs](https://gemarcade.com/developer/api/server#gemmatchserver-roomargs)
4232
+
4233
+ ```ts
4234
+ readonly roomArgs: Readonly<Record<string, RoomArgValue>> | undefined
4235
+ ```
4236
+
4237
+ **Deprecated.** Use `gem.room?.args()` instead. It reads the same settings from
4238
+ the platform and can tell "this room type has no settings" apart from "it has
4239
+ settings and they are empty" — a difference this snapshot cannot express.
4240
+
4241
+ Still delivered today. The member keeps compiling for the rest of `3.x`,
4242
+ and `4.0.0` is the earliest release that may take it off the type. Whether
4243
+ a server keeps sending a value is a separate question, decided where the
4244
+ dedicated-server image is built rather than by a package release — so check
4245
+ for the room read and keep this as the fallback. Both halves are in
4246
+ [the deprecation policy](https://gemarcade.com/developer/docs/deprecation-policy).
4247
+
4248
+ The room's settings as they were at match start.
4249
+
4250
+ ## GemMatchServer.roster
4251
+
4252
+ [GemMatchServer.roster](https://gemarcade.com/developer/api/server#gemmatchserver-roster)
4253
+
4254
+ ```ts
4255
+ readonly roster: readonly string[]
4256
+ ```
4257
+
4258
+ **Deprecated.** Use `gem.room?.get()` instead: live membership, with names, roles
4259
+ and ready state.
4260
+
4261
+ Still delivered today. The member keeps compiling for the rest of `3.x`,
4262
+ and `4.0.0` is the earliest release that may take it off the type. Whether
4263
+ a server keeps sending a value is a separate question, decided where the
4264
+ dedicated-server image is built rather than by a package release — so check
4265
+ for the room read and keep this as the fallback. Both halves are in
4266
+ [the deprecation policy](https://gemarcade.com/developer/docs/deprecation-policy).
4267
+
4268
+ Who was in the room when the server was allocated.
4269
+
4270
+ A HINT FOR SIZING, NOT A GUEST LIST. It is never revised, and it is not what
4271
+ decides who may connect — the platform admits players by credential, not by
4272
+ this array.
4273
+
4274
+ ## GemMatchServer.players
4275
+
4276
+ [GemMatchServer.players](https://gemarcade.com/developer/api/server#gemmatchserver-players)
4277
+
4278
+ ```ts
4279
+ players(): string[]
4280
+ ```
4281
+
4282
+ The players connected right now, as a fresh array each call.
4283
+
4284
+ ## GemMatchServer.profile
4285
+
4286
+ [GemMatchServer.profile](https://gemarcade.com/developer/api/server#gemmatchserver-profile)
4287
+
4288
+ ```ts
4289
+ profile(playerId: string): GemServerProfile | null
4290
+ ```
4291
+
4292
+ A connected player's display identity, or `null` if the platform has none
4293
+ for them.
4294
+
4295
+ | Parameter | Type | Description |
4296
+ | --- | --- | --- |
4297
+ | `playerId` | `string` | Take this from `join()` or `players()`, never from something
4298
+ a client sent you. |
4299
+
4300
+ ## GemMatchServer.playerNumber
4301
+
4302
+ [GemMatchServer.playerNumber](https://gemarcade.com/developer/api/server#gemmatchserver-playernumber)
4303
+
4304
+ ```ts
4305
+ playerNumber(playerId: string): number | undefined
4306
+ ```
4307
+
4308
+ The seat number the platform gave a player — 1-based.
4309
+
4310
+ `undefined` when there is no answer: an id that is not seated, or an older
4311
+ platform image that does not assign seats. You cannot tell those apart and
4312
+ do not need to — handle the absence.
4313
+
4314
+ | Parameter | Type |
4315
+ | --- | --- |
4316
+ | `playerId` | `string` |
4317
+
4318
+ ## GemMatchServer.send
4319
+
4320
+ [GemMatchServer.send](https://gemarcade.com/developer/api/server#gemmatchserver-send)
4321
+
4322
+ ```ts
4323
+ send(playerId: string, data: string | Uint8Array<ArrayBufferLike>, options?: SendOptions): void
4324
+ ```
4325
+
4326
+ Send to one player.
4327
+
4328
+ | Parameter | Type | Description |
4329
+ | --- | --- | --- |
4330
+ | `playerId` | `string` | Who to send to. |
4331
+ | `data` | `string \| Uint8Array<ArrayBufferLike>` | A string or bytes. The receiver is told which. |
4332
+ | `options` | `SendOptions` | What delivery you are ASKING FOR — see [SendOptions](https://gemarcade.com/developer/api/server#sendoptions).
4333
+ Both default to true. Pass them freely: an older platform image ignores
4334
+ them harmlessly. |
4335
+
4336
+ ## GemMatchServer.broadcast
4337
+
4338
+ [GemMatchServer.broadcast](https://gemarcade.com/developer/api/server#gemmatchserver-broadcast)
4339
+
4340
+ ```ts
4341
+ broadcast(data: string | Uint8Array<ArrayBufferLike>, options?: BroadcastOptions): void
4342
+ ```
4343
+
4344
+ Send to every connected player.
4345
+
4346
+ | Parameter | Type | Description |
4347
+ | --- | --- | --- |
4348
+ | `data` | `string \| Uint8Array<ArrayBufferLike>` | A string or bytes. |
4349
+ | `options` | `BroadcastOptions` | See [BroadcastOptions](https://gemarcade.com/developer/api/server#broadcastoptions) — the same delivery choices as
4350
+ [GemMatchServer.send](https://gemarcade.com/developer/api/server#gemmatchserver-send), plus who to leave out. |
4351
+
4352
+ ## GemMatchServer.kick
4353
+
4354
+ [GemMatchServer.kick](https://gemarcade.com/developer/api/server#gemmatchserver-kick)
4355
+
4356
+ ```ts
4357
+ kick(playerId: string, reason?: string): void
4358
+ ```
4359
+
4360
+ Disconnect a player.
4361
+
4362
+ | Parameter | Type | Description |
4363
+ | --- | --- | --- |
4364
+ | `playerId` | `string` | Who to remove. |
4365
+ | `reason` | `string` | Shown to them, and recorded in the match's logs. |
4366
+
4367
+ ## GemMatchServer.log
4368
+
4369
+ [GemMatchServer.log](https://gemarcade.com/developer/api/server#gemmatchserver-log)
4370
+
4371
+ ```ts
4372
+ log(message: string): void
4373
+ ```
4374
+
4375
+ Write a line to the match's logs.
4376
+
4377
+ USE THIS, NEVER `console.log`. Standard output belongs to the platform — it
4378
+ is how the runtime talks to your server — and writing to it directly
4379
+ corrupts that conversation.
4380
+
4381
+ | Parameter | Type |
4382
+ | --- | --- |
4383
+ | `message` | `string` |
4384
+
4385
+ ## GemMatchServer.warn
4386
+
4387
+ [GemMatchServer.warn](https://gemarcade.com/developer/api/server#gemmatchserver-warn)
4388
+
4389
+ ```ts
4390
+ warn(message: string): void
4391
+ ```
4392
+
4393
+ Like [GemMatchServer.log](https://gemarcade.com/developer/api/server#gemmatchserver-log), marked as a warning.
4394
+
4395
+ | Parameter | Type |
4396
+ | --- | --- |
4397
+ | `message` | `string` |