@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.
- package/README.md +54 -0
- package/dist/game.d.ts +156 -22
- package/dist/gem-web-sdk.bridge.js +1 -1
- package/dist/gem-web-sdk.esm.js +2 -2
- package/dist/gem-web-sdk.game.js +2 -2
- package/dist/gem-web-sdk.min.js +2 -2
- package/dist/gem-web-sdk.sdk.js +2 -2
- package/dist/gem-web-sdk.server.js +2 -2
- package/dist/index.d.ts +89 -6
- package/dist/sdk.d.ts +156 -22
- package/docs/achievements.md +232 -0
- package/docs/analytics.md +271 -0
- package/docs/api/api-reference-game.md +15308 -0
- package/docs/api/api-reference-server.md +4397 -0
- package/docs/api/reference.json +24130 -0
- package/docs/arcade-economy.md +263 -0
- package/docs/async-opponents.md +396 -0
- package/docs/brand-art.md +334 -0
- package/docs/cross-promotion.md +137 -0
- package/docs/daily-challenges.md +530 -0
- package/docs/dedicated-server-flow.md +879 -0
- package/docs/deprecation-policy.md +124 -0
- package/docs/friends.md +233 -0
- package/docs/game-economy-tutorial.md +326 -0
- package/docs/getting-started.md +552 -0
- package/docs/inventory-and-catalogs.md +1035 -0
- package/docs/leaderboards.md +277 -0
- package/docs/manifest.json +135 -0
- package/docs/match-session.md +288 -0
- package/docs/p2p-and-parties.md +783 -0
- package/docs/platform-rules.md +372 -0
- package/docs/player-sign-in.md +89 -0
- package/docs/room-shapes.md +195 -0
- package/docs/rooms-and-matches.md +779 -0
- package/docs/saving-player-progress.md +457 -0
- package/docs/server-bundles.md +1041 -0
- package/docs/serverless-functions.md +748 -0
- package/docs/trading-and-gifting.md +98 -0
- package/docs/what-a-game-must-do.md +1231 -0
- package/package.json +7 -2
- package/schema/bridge.v1.json +440 -7
- 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` |
|