@crowdedkingdoms/crowdyjs 18.8.0-dev.1 → 18.9.0-dev.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/MIGRATION.md CHANGED
@@ -120,6 +120,80 @@ super-admin's session). There is no SDK replacement.
120
120
  `dev`'s SDL after #417 merged (`npm run schema:sync:paths -- --schema <that schema.gql>`, then
121
121
  `npm run codegen`).
122
122
 
123
+ ## 18.9.0: the input log says what it lost, and when to retry
124
+
125
+ Additive. **Needs the Game API release after v2.40.2** (`InputLogSession.missingRecords` is
126
+ selected on every sessions read; an older Game API refuses the selection). Promote the Game API
127
+ first on each tier.
128
+
129
+ - **`missingRecords`** on every recorded session: inputs the replication server accepted that
130
+ never reached the log (an overloaded server or a broker outage). 0 when the recording is
131
+ complete as far as it is known: a lost end of the recording cannot be counted (its session
132
+ closes as `unrecorded` instead). Null on one recorded before the count existed. Recording is
133
+ best-effort, and this is how a gap shows.
134
+ - **Three end reasons** in `endReason`: `logging_off` (replay logging was turned off during the
135
+ session), `shutdown` (the replication server recording it stopped, a deploy; the client
136
+ reconnects to another server, where the same session continues and its end is replaced), and
137
+ `paused` (recording paused because the app's logging setting could not be confirmed; the
138
+ client sees nothing, and a later record reopens the session). `unrecorded` (no end was
139
+ recorded; closed an hour after its last input) comes with the same Game API release.
140
+ - **Reads say when to retry.** `messages` throws `INPUT_LOG_TEMPORARILY_UNAVAILABLE` when the log
141
+ cannot be read right now (it used to answer an empty page, or `INPUT_LOG_UNAVAILABLE`). A server
142
+ runs one read per user, and two per app and per org, at a time: a read over that share waits
143
+ for an earlier one within its own few-second deadline, and `INPUT_LOG_RATE_LIMITED` means the
144
+ wait ran out. Both carry `extensions.retryable`: retry with the same cursor after a short
145
+ back-off, a bounded number of times. A read whose request is closed stops and frees its
146
+ share: pass `signal` in the options of `sessions` / `messages` (new) and abort a read nobody
147
+ will see, say when the user opens another session. An operation may select
148
+ `inputLogMessages` only once.
149
+ - **A `signal` keeps the request timeout.** A `signal` passed to a request
150
+ (`client.gql.request(document, variables, { signal })`, and the input log's new option) used
151
+ to replace the client's timeout, so a request with one could wait for ever, and an abort
152
+ rejected as `CrowdyTimeoutError`. It now runs alongside the timeout, and aborting rejects with
153
+ the signal's reason.
154
+ - **A cursor belongs to its session.** `BAD_USER_INPUT` for a malformed cursor or one from another
155
+ session (until this Game API, a cursor from another session was not detected and the read just
156
+ returned fewer inputs).
157
+ - **`apps.update(appId, { replayLoggingEnabled: true })`** needs a spendable balance that covers
158
+ the projected cost of keeping one retention period of the app's recent traffic; the
159
+ `INPUT_LOG_FUNDS_NEEDED` refusal's `extensions.requiredMicrousd` and `spendableMicrousd` say how
160
+ much. Recordings already kept go on billing after logging is turned off, until they age out.
161
+
162
+ Paging a session, the loop that does not stop early:
163
+
164
+ ```ts
165
+ import { CrowdyGraphQLError, decodeBase64 } from '@crowdedkingdoms/crowdyjs';
166
+
167
+ const RETRY = new Set(['INPUT_LOG_TEMPORARILY_UNAVAILABLE', 'INPUT_LOG_RATE_LIMITED']);
168
+ const MAX_RETRIES = 8; // in a row, about half a minute of back-off; then give up
169
+ const reading = new AbortController(); // abort() when the user moves on
170
+ let after: string | undefined;
171
+ for (let backoffMs = 250, retries = 0; ; ) {
172
+ let page;
173
+ try {
174
+ page = await game.inputLog.messages(appId, gameTokenId, {
175
+ first: 200,
176
+ after,
177
+ signal: reading.signal,
178
+ });
179
+ } catch (err) {
180
+ if (!(err instanceof CrowdyGraphQLError) || !RETRY.has(String(err.code))) throw err;
181
+ if (++retries > MAX_RETRIES) throw err;
182
+ await new Promise((r) => setTimeout(r, backoffMs));
183
+ backoffMs = Math.min(backoffMs * 2, 5_000);
184
+ continue; // the same cursor: nothing is skipped
185
+ }
186
+ backoffMs = 250;
187
+ retries = 0;
188
+ for (const { node } of page.edges) handle(node.messageType, decodeBase64(node.body));
189
+ if (!page.pageInfo.hasNextPage) break;
190
+ after = page.pageInfo.endCursor ?? after;
191
+ }
192
+ ```
193
+
194
+ Nothing in the SDK changed shape; the regenerated types add `missingRecords` and the refreshed
195
+ descriptions.
196
+
123
197
  ## 18.8.0: builds report the SDK version they compiled against
124
198
 
125
199
  Additive, with one new floor: **the build documents select `ExecBuild.sdkVersion`, so 18.8.0
@@ -230,16 +304,21 @@ Additive. An app with replay logging on has its client inputs recorded, and `cli
230
304
  reads them back on the app-scoped client:
231
305
 
232
306
  ```ts
233
- const { edges, pageInfo } = await game.inputLog.sessions(appId, { first: 20 });
307
+ const { edges } = await game.inputLog.sessions(appId, { first: 20 });
308
+ const gameTokenId = edges[0].node.gameTokenId;
234
309
  let after: string | undefined;
235
- do {
236
- const page = await game.inputLog.messages(appId, edges[0].node.gameTokenId, { first: 200, after });
310
+ for (;;) {
311
+ const page = await game.inputLog.messages(appId, gameTokenId, { first: 200, after });
237
312
  for (const { node } of page.edges) handle(node.messageType, decodeBase64(node.body));
238
- after = page.pageInfo.endCursor ?? undefined;
239
313
  if (!page.pageInfo.hasNextPage) break;
240
- } while (after);
314
+ after = page.pageInfo.endCursor ?? after;
315
+ }
241
316
  ```
242
317
 
318
+ (Corrected in 18.9.0: the first version of this example stopped when `endCursor` came back null
319
+ even with `hasNextPage` true, which a v2.39 Game API answers when a read cannot start in time.
320
+ 18.9.0's section has the loop with retries.)
321
+
243
322
  A player reads only the sessions and inputs they sent; a holder of `manage_apps` on the app reads
244
323
  every session. Keep paging while `hasNextPage` is true: a messages page can be short, or empty,
245
324
  when it stopped at the server's time or scan limit. Inputs are kept for the published retention,
@@ -250,8 +329,8 @@ with the same cursor, when the log cannot be read right now.
250
329
  `App.replayLoggingEnabled` is selected on every app read and set with `apps.update(appId,
251
330
  { replayLoggingEnabled: true })` (`manage_apps`). Turning it on is refused with
252
331
  `INPUT_LOG_FUNDS_NEEDED` unless the org's wallet has a spendable balance or the org is exempt from
253
- billing, because stored input logs are billed (crowdedkingdoms.com/pricing). It needs ck-api with
254
- the input log (`inputLogSessions`, `inputLogMessages`).
332
+ billing, because stored input logs are billed (crowdedkingdoms.com/pricing). It needs ck-api v2.39.0 or later: every app read selects
333
+ `replayLoggingEnabled`, which an older Game API refuses.
255
334
 
256
335
  ## 18.5.0: channel messages limited by distance
257
336
 
package/README.md CHANGED
@@ -29,6 +29,16 @@ This README is the `dev` branch's: it documents the release on the `dev` dist-ta
29
29
  (`npm view @crowdedkingdoms/crowdyjs dist-tags`), and its links go to the dev docs site. A
30
30
  plain `npm install` resolves `latest`, the production release, which has no `client.exec`.
31
31
 
32
+ **Server compatibility.** A release selects fields the Game API it was built against has,
33
+ so it needs that Game API or newer; promote the Game API before the SDK on each tier.
34
+
35
+ | CrowdyJS | Needs the Game API (ck-api) |
36
+ | --- | --- |
37
+ | 18.9.0 | the release after v2.40.2 (`InputLogSession.missingRecords`) |
38
+ | 18.8.0 | v2.40.2 or later (`ExecBuild.sdkVersion`) |
39
+ | 18.7.0 | v2.40.0 or later (`runtimeGate`, channel audio) |
40
+ | 18.6.0 | v2.39.0 or later (every app read selects `replayLoggingEnabled`) |
41
+
32
42
  CrowdyJS targets browsers by default and uses native `fetch`, `WebSocket`,
33
43
  `crypto`, `btoa`, and `atob`. Node tools can still use the SDK, but must
34
44
  provide browser-compatible globals when opening realtime connections (e.g.
package/dist/client.d.ts CHANGED
@@ -147,7 +147,8 @@ export declare class GraphQLClient {
147
147
  * @param variables - Variables for the operation; omit for operations that
148
148
  * take none.
149
149
  * @param options - Optional `signal` to abort the request from your own
150
- * `AbortController` (in addition to the built-in timeout).
150
+ * `AbortController` (in addition to the built-in timeout). Aborting it rejects
151
+ * with the signal's reason, not a {@link CrowdyTimeoutError}.
151
152
  * @returns The operation's `data` payload.
152
153
  * @throws {CrowdyHttpError} on a non-2xx HTTP status.
153
154
  * @throws {CrowdyGraphQLError} when the response carries a non-empty
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAahD,OAAO,EAAkB,KAAK,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAG1D;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;;;OAYG;IACH,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACzE;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,aAAa;IACxB,yEAAyE;IACzE,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IACvC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAgB;IAC/C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAe;IAC1C,OAAO,CAAC,iBAAiB,CAAC,CAEM;IAEhC,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED;;;;;;OAMG;gBACS,MAAM,EAAE,mBAAmB,YAAK,EAAE,OAAO,EAAE,YAAY;IAmBnE;;;;;;;OAOG;IACH,yBAAyB,CACvB,OAAO,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GAC5D,IAAI;IAIP;;;;OAIG;IACH,WAAW,IAAI,MAAM;IAIrB;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAInC;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,OAAO,CAAC,OAAO,EAAE,UAAU,EAC/B,QAAQ,EAAE,iBAAiB,CAAC,OAAO,EAAE,UAAU,CAAC,EAChD,SAAS,CAAC,EAAE,UAAU,EACtB,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAO,GACrC,OAAO,CAAC,OAAO,CAAC;IASnB;;;;;;;;;;;;;;;;;;;OAmBG;IACG,KAAK,CAAC,CAAC,GAAG,GAAG,EACjB,KAAK,EAAE,MAAM,EACb,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACvC,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAO,GACrC,OAAO,CAAC,CAAC,CAAC;IA0Cb,0EAA0E;YAC5D,OAAO;CAyEtB;AAED;;;GAGG;AACH,OAAO,EAAE,aAAa,IAAI,gBAAgB,EAAE,CAAC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAahD,OAAO,EAAkB,KAAK,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAG1D;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;;;OAYG;IACH,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACzE;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,aAAa;IACxB,yEAAyE;IACzE,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IACvC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAgB;IAC/C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAe;IAC1C,OAAO,CAAC,iBAAiB,CAAC,CAEM;IAEhC,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED;;;;;;OAMG;gBACS,MAAM,EAAE,mBAAmB,YAAK,EAAE,OAAO,EAAE,YAAY;IAmBnE;;;;;;;OAOG;IACH,yBAAyB,CACvB,OAAO,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GAC5D,IAAI;IAIP;;;;OAIG;IACH,WAAW,IAAI,MAAM;IAIrB;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAInC;;;;;;;;;;;;;;;;;;;;;OAqBG;IACG,OAAO,CAAC,OAAO,EAAE,UAAU,EAC/B,QAAQ,EAAE,iBAAiB,CAAC,OAAO,EAAE,UAAU,CAAC,EAChD,SAAS,CAAC,EAAE,UAAU,EACtB,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAO,GACrC,OAAO,CAAC,OAAO,CAAC;IASnB;;;;;;;;;;;;;;;;;;;OAmBG;IACG,KAAK,CAAC,CAAC,GAAG,GAAG,EACjB,KAAK,EAAE,MAAM,EACb,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACvC,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAO,GACrC,OAAO,CAAC,CAAC,CAAC;IA0Cb,0EAA0E;YAC5D,OAAO;CAqFtB;AAED;;;GAGG;AACH,OAAO,EAAE,aAAa,IAAI,gBAAgB,EAAE,CAAC"}
package/dist/client.js CHANGED
@@ -107,7 +107,8 @@ export class GraphQLClient {
107
107
  * @param variables - Variables for the operation; omit for operations that
108
108
  * take none.
109
109
  * @param options - Optional `signal` to abort the request from your own
110
- * `AbortController` (in addition to the built-in timeout).
110
+ * `AbortController` (in addition to the built-in timeout). Aborting it rejects
111
+ * with the signal's reason, not a {@link CrowdyTimeoutError}.
111
112
  * @returns The operation's `data` payload.
112
113
  * @throws {CrowdyHttpError} on a non-2xx HTTP status.
113
114
  * @throws {CrowdyGraphQLError} when the response carries a non-empty
@@ -180,9 +181,20 @@ export class GraphQLClient {
180
181
  /** One HTTP round trip. {@link query} wraps this to handle a redirect. */
181
182
  async attempt(query, variables, options) {
182
183
  const controller = new AbortController();
183
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
184
+ let timedOut = false;
185
+ const timeoutId = setTimeout(() => {
186
+ timedOut = true;
187
+ controller.abort();
188
+ }, this.timeout);
189
+ // The caller's signal is followed as well as the timeout, never instead of it.
190
+ const caller = options.signal;
191
+ const followCaller = () => controller.abort(caller?.reason);
192
+ if (caller?.aborted)
193
+ followCaller();
194
+ else
195
+ caller?.addEventListener('abort', followCaller, { once: true });
184
196
  const token = this.session.getToken();
185
- const signal = options.signal ?? controller.signal;
197
+ const signal = controller.signal;
186
198
  const lbCookie = this.lbCookieStore?.headerValue();
187
199
  try {
188
200
  const requestBody = { query, variables };
@@ -227,6 +239,8 @@ export class GraphQLClient {
227
239
  }
228
240
  catch (error) {
229
241
  clearTimeout(timeoutId);
242
+ if (!timedOut && caller?.aborted)
243
+ throw caller.reason ?? error;
230
244
  if (error instanceof Error && error.name === 'AbortError') {
231
245
  throw new CrowdyTimeoutError(this.timeout);
232
246
  }
@@ -239,6 +253,9 @@ export class GraphQLClient {
239
253
  this.logger.error?.('GraphQL request failed', error);
240
254
  throw new CrowdyNetworkError(error);
241
255
  }
256
+ finally {
257
+ caller?.removeEventListener('abort', followCaller);
258
+ }
242
259
  }
243
260
  }
244
261
  /**
@@ -15,6 +15,13 @@ export interface InputLogPageOptions<Filter> {
15
15
  /** A previous page's `pageInfo.endCursor`; omit for the first page. */
16
16
  after?: string;
17
17
  filter?: Filter;
18
+ /**
19
+ * Abort the read: the call rejects with the signal's reason, and the server stops a
20
+ * `messages` read and frees its share at once (the Game API release after v2.40.2), so the
21
+ * next read, a different session's say, does not wait for it. Abort the one you no longer
22
+ * need when the user moves on.
23
+ */
24
+ signal?: AbortSignal;
18
25
  }
19
26
  /**
20
27
  * The input log: the client inputs the realtime servers recorded for an app with
@@ -28,11 +35,21 @@ export interface InputLogPageOptions<Filter> {
28
35
  * **Who sees what.** A player reads only the sessions and inputs they sent. A holder of
29
36
  * `manage_apps` on the app reads every session and may filter by `userId`.
30
37
  *
31
- * Inputs are kept for the input log's published retention (crowdedkingdoms.com/pricing)
32
- * and then deleted, so an old session can still be listed after its inputs are gone.
33
- * Without input logging on the deployment, both calls throw
34
- * {@link CrowdyGraphQLError} `INPUT_LOG_UNAVAILABLE`; `messages` also throws it, retryable
35
- * with the same cursor, when the log cannot be read right now.
38
+ * Inputs are kept for the input log's published retention (crowdedkingdoms.com/pricing,
39
+ * up to about an hour longer) and then deleted, so an old session can still be listed
40
+ * after its inputs are gone. Recording is best-effort: a session's `missingRecords` counts
41
+ * the inputs that were accepted but never reached the log.
42
+ *
43
+ * Without input logging on the deployment, both calls throw {@link CrowdyGraphQLError}
44
+ * `INPUT_LOG_UNAVAILABLE`. `messages` throws `INPUT_LOG_TEMPORARILY_UNAVAILABLE` when the
45
+ * log cannot be read right now. A server runs one read per user, and two per app and per
46
+ * org, at a time: a read over that share waits for an earlier one within its own few-second
47
+ * deadline, and `INPUT_LOG_RATE_LIMITED` means the wait ran out. Both carry
48
+ * `extensions.retryable`, so wait a moment and call again with the same cursor; a read
49
+ * whose request is closed (`opts.signal` aborted) stops and frees its share. An operation may select
50
+ * `inputLogMessages` only once. The Game API release after v2.40.2 raises those two;
51
+ * v2.39.0 to v2.40.2 answer `INPUT_LOG_UNAVAILABLE` instead, and an empty page when a read
52
+ * could not start in time.
36
53
  */
37
54
  export declare class InputLogAPI {
38
55
  private gql;
@@ -44,7 +61,18 @@ export declare class InputLogAPI {
44
61
  * @param opts - Paging, and {@link InputLogSessionFilter}: `userId` (another user's
45
62
  * needs `manage_apps`), `from` / `to`, and `messageType` (sessions containing it).
46
63
  * @returns One page: `edges[].node` are the sessions, `pageInfo.endCursor` the next
47
- * page's `after`, `totalCount` the matching sessions.
64
+ * page's `after`, `totalCount` the matching sessions. A node's `endReason` is how the
65
+ * session ended: `expired`, `revoked`, `reconnect` or `released` as the client saw it;
66
+ * `logging_off` when the app turned replay logging off during it; `shutdown` when the
67
+ * realtime server recording it stopped (the client reconnects to another server, where
68
+ * the same session continues and its end is replaced); `paused` when recording paused
69
+ * because the app's logging setting could not be confirmed (a later record reopens the
70
+ * session); `unrecorded` when no end was recorded and it was closed an hour after its last
71
+ * input (all three from the Game API release after v2.40.2); or null while it may still
72
+ * run. `missingRecords` counts the session's inputs that were accepted but never recorded,
73
+ * by an overloaded server or a broker outage: 0 when the recording is complete as far as
74
+ * it is known (a lost end cannot be counted), null on a session recorded before the count
75
+ * existed.
48
76
  * @throws {CrowdyGraphQLError} `FORBIDDEN` for another user's sessions without
49
77
  * `manage_apps`, or `INPUT_LOG_UNAVAILABLE`.
50
78
  */
@@ -57,8 +85,12 @@ export declare class InputLogAPI {
57
85
  * input logs are billed on. Spatial inputs also carry their chunk and actor, and
58
86
  * channel inputs their channel.
59
87
  *
60
- * **Keep paging while `pageInfo.hasNextPage` is true.** A page can hold fewer than
61
- * `first` inputs, or none, when it reached the server's time or scan limit.
88
+ * **Keep paging while `pageInfo.hasNextPage` is true**, not while `endCursor` is set: a page
89
+ * can hold fewer than `first` inputs, or none, when it reached the server's time or scan
90
+ * limit, and its `endCursor` still moves the next page on. Keep the previous cursor if one
91
+ * comes back null, and retry `INPUT_LOG_TEMPORARILY_UNAVAILABLE` / `INPUT_LOG_RATE_LIMITED`
92
+ * with it after a short back-off, a bounded number of times (MIGRATION.md, 18.9.0). Pass
93
+ * `opts.signal` and abort a read nobody will see.
62
94
  *
63
95
  * @param appId - Numeric app id.
64
96
  * @param gameTokenId - The session: {@link InputLogSessionRecord.gameTokenId}.
@@ -66,8 +98,10 @@ export declare class InputLogAPI {
66
98
  * `messageTypes` (at most 64).
67
99
  * @returns One page of inputs.
68
100
  * @throws {CrowdyGraphQLError} `NOT_FOUND` for a session that is not yours without
69
- * `manage_apps`, `BAD_USER_INPUT` for a cursor from another session, or
70
- * `INPUT_LOG_UNAVAILABLE`.
101
+ * `manage_apps`; `BAD_USER_INPUT` for a malformed cursor, or (from the Game API release
102
+ * after v2.40.2, which binds a cursor to its session) one from another session;
103
+ * `INPUT_LOG_TEMPORARILY_UNAVAILABLE` or `INPUT_LOG_RATE_LIMITED`, both retryable with the
104
+ * same cursor; or `INPUT_LOG_UNAVAILABLE`.
71
105
  */
72
106
  messages(appId: string, gameTokenId: string, opts?: InputLogPageOptions<InputLogMessageFilter>): Promise<InputLogMessagePage>;
73
107
  }
@@ -1 +1 @@
1
- {"version":3,"file":"inputLog.d.ts","sourceRoot":"","sources":["../../src/domains/inputLog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,OAAO,EAGL,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAEjC,gDAAgD;AAChD,MAAM,MAAM,mBAAmB,GAAG,qBAAqB,CAAC,kBAAkB,CAAC,CAAC;AAC5E,wEAAwE;AACxE,MAAM,MAAM,qBAAqB,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;AACjF,gDAAgD;AAChD,MAAM,MAAM,mBAAmB,GAAG,qBAAqB,CAAC,kBAAkB,CAAC,CAAC;AAC5E,sEAAsE;AACtE,MAAM,MAAM,qBAAqB,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjF,4FAA4F;AAC5F,MAAM,WAAW,mBAAmB,CAAC,MAAM;IACzC,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,WAAW;IACV,OAAO,CAAC,GAAG;gBAAH,GAAG,EAAE,aAAa;IAEtC;;;;;;;;;;OAUG;IACG,QAAQ,CACZ,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,mBAAmB,CAAC,qBAAqB,CAAM,GACpD,OAAO,CAAC,mBAAmB,CAAC;IAU/B;;;;;;;;;;;;;;;;;;;OAmBG;IACG,QAAQ,CACZ,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,IAAI,GAAE,mBAAmB,CAAC,qBAAqB,CAAM,GACpD,OAAO,CAAC,mBAAmB,CAAC;CAUhC"}
1
+ {"version":3,"file":"inputLog.d.ts","sourceRoot":"","sources":["../../src/domains/inputLog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,OAAO,EAGL,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAEjC,gDAAgD;AAChD,MAAM,MAAM,mBAAmB,GAAG,qBAAqB,CAAC,kBAAkB,CAAC,CAAC;AAC5E,wEAAwE;AACxE,MAAM,MAAM,qBAAqB,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;AACjF,gDAAgD;AAChD,MAAM,MAAM,mBAAmB,GAAG,qBAAqB,CAAC,kBAAkB,CAAC,CAAC;AAC5E,sEAAsE;AACtE,MAAM,MAAM,qBAAqB,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjF,4FAA4F;AAC5F,MAAM,WAAW,mBAAmB,CAAC,MAAM;IACzC,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,WAAW;IACV,OAAO,CAAC,GAAG;gBAAH,GAAG,EAAE,aAAa;IAEtC;;;;;;;;;;;;;;;;;;;;;OAqBG;IACG,QAAQ,CACZ,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,mBAAmB,CAAC,qBAAqB,CAAM,GACpD,OAAO,CAAC,mBAAmB,CAAC;IAS/B;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACG,QAAQ,CACZ,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,IAAI,GAAE,mBAAmB,CAAC,qBAAqB,CAAM,GACpD,OAAO,CAAC,mBAAmB,CAAC;CAQhC"}
@@ -11,11 +11,21 @@ import { InputLogMessagesDocument, InputLogSessionsDocument, } from '../generate
11
11
  * **Who sees what.** A player reads only the sessions and inputs they sent. A holder of
12
12
  * `manage_apps` on the app reads every session and may filter by `userId`.
13
13
  *
14
- * Inputs are kept for the input log's published retention (crowdedkingdoms.com/pricing)
15
- * and then deleted, so an old session can still be listed after its inputs are gone.
16
- * Without input logging on the deployment, both calls throw
17
- * {@link CrowdyGraphQLError} `INPUT_LOG_UNAVAILABLE`; `messages` also throws it, retryable
18
- * with the same cursor, when the log cannot be read right now.
14
+ * Inputs are kept for the input log's published retention (crowdedkingdoms.com/pricing,
15
+ * up to about an hour longer) and then deleted, so an old session can still be listed
16
+ * after its inputs are gone. Recording is best-effort: a session's `missingRecords` counts
17
+ * the inputs that were accepted but never reached the log.
18
+ *
19
+ * Without input logging on the deployment, both calls throw {@link CrowdyGraphQLError}
20
+ * `INPUT_LOG_UNAVAILABLE`. `messages` throws `INPUT_LOG_TEMPORARILY_UNAVAILABLE` when the
21
+ * log cannot be read right now. A server runs one read per user, and two per app and per
22
+ * org, at a time: a read over that share waits for an earlier one within its own few-second
23
+ * deadline, and `INPUT_LOG_RATE_LIMITED` means the wait ran out. Both carry
24
+ * `extensions.retryable`, so wait a moment and call again with the same cursor; a read
25
+ * whose request is closed (`opts.signal` aborted) stops and frees its share. An operation may select
26
+ * `inputLogMessages` only once. The Game API release after v2.40.2 raises those two;
27
+ * v2.39.0 to v2.40.2 answer `INPUT_LOG_UNAVAILABLE` instead, and an empty page when a read
28
+ * could not start in time.
19
29
  */
20
30
  export class InputLogAPI {
21
31
  constructor(gql) {
@@ -28,17 +38,23 @@ export class InputLogAPI {
28
38
  * @param opts - Paging, and {@link InputLogSessionFilter}: `userId` (another user's
29
39
  * needs `manage_apps`), `from` / `to`, and `messageType` (sessions containing it).
30
40
  * @returns One page: `edges[].node` are the sessions, `pageInfo.endCursor` the next
31
- * page's `after`, `totalCount` the matching sessions.
41
+ * page's `after`, `totalCount` the matching sessions. A node's `endReason` is how the
42
+ * session ended: `expired`, `revoked`, `reconnect` or `released` as the client saw it;
43
+ * `logging_off` when the app turned replay logging off during it; `shutdown` when the
44
+ * realtime server recording it stopped (the client reconnects to another server, where
45
+ * the same session continues and its end is replaced); `paused` when recording paused
46
+ * because the app's logging setting could not be confirmed (a later record reopens the
47
+ * session); `unrecorded` when no end was recorded and it was closed an hour after its last
48
+ * input (all three from the Game API release after v2.40.2); or null while it may still
49
+ * run. `missingRecords` counts the session's inputs that were accepted but never recorded,
50
+ * by an overloaded server or a broker outage: 0 when the recording is complete as far as
51
+ * it is known (a lost end cannot be counted), null on a session recorded before the count
52
+ * existed.
32
53
  * @throws {CrowdyGraphQLError} `FORBIDDEN` for another user's sessions without
33
54
  * `manage_apps`, or `INPUT_LOG_UNAVAILABLE`.
34
55
  */
35
56
  async sessions(appId, opts = {}) {
36
- const data = await this.gql.request(InputLogSessionsDocument, {
37
- appId,
38
- first: opts.first,
39
- after: opts.after,
40
- filter: opts.filter,
41
- });
57
+ const data = await this.gql.request(InputLogSessionsDocument, { appId, first: opts.first, after: opts.after, filter: opts.filter }, { signal: opts.signal });
42
58
  return data.inputLogSessions;
43
59
  }
44
60
  /**
@@ -49,8 +65,12 @@ export class InputLogAPI {
49
65
  * input logs are billed on. Spatial inputs also carry their chunk and actor, and
50
66
  * channel inputs their channel.
51
67
  *
52
- * **Keep paging while `pageInfo.hasNextPage` is true.** A page can hold fewer than
53
- * `first` inputs, or none, when it reached the server's time or scan limit.
68
+ * **Keep paging while `pageInfo.hasNextPage` is true**, not while `endCursor` is set: a page
69
+ * can hold fewer than `first` inputs, or none, when it reached the server's time or scan
70
+ * limit, and its `endCursor` still moves the next page on. Keep the previous cursor if one
71
+ * comes back null, and retry `INPUT_LOG_TEMPORARILY_UNAVAILABLE` / `INPUT_LOG_RATE_LIMITED`
72
+ * with it after a short back-off, a bounded number of times (MIGRATION.md, 18.9.0). Pass
73
+ * `opts.signal` and abort a read nobody will see.
54
74
  *
55
75
  * @param appId - Numeric app id.
56
76
  * @param gameTokenId - The session: {@link InputLogSessionRecord.gameTokenId}.
@@ -58,17 +78,13 @@ export class InputLogAPI {
58
78
  * `messageTypes` (at most 64).
59
79
  * @returns One page of inputs.
60
80
  * @throws {CrowdyGraphQLError} `NOT_FOUND` for a session that is not yours without
61
- * `manage_apps`, `BAD_USER_INPUT` for a cursor from another session, or
62
- * `INPUT_LOG_UNAVAILABLE`.
81
+ * `manage_apps`; `BAD_USER_INPUT` for a malformed cursor, or (from the Game API release
82
+ * after v2.40.2, which binds a cursor to its session) one from another session;
83
+ * `INPUT_LOG_TEMPORARILY_UNAVAILABLE` or `INPUT_LOG_RATE_LIMITED`, both retryable with the
84
+ * same cursor; or `INPUT_LOG_UNAVAILABLE`.
63
85
  */
64
86
  async messages(appId, gameTokenId, opts = {}) {
65
- const data = await this.gql.request(InputLogMessagesDocument, {
66
- appId,
67
- gameTokenId,
68
- first: opts.first,
69
- after: opts.after,
70
- filter: opts.filter,
71
- });
87
+ const data = await this.gql.request(InputLogMessagesDocument, { appId, gameTokenId, first: opts.first, after: opts.after, filter: opts.filter }, { signal: opts.signal });
72
88
  return data.inputLogMessages;
73
89
  }
74
90
  }
@@ -882,7 +882,7 @@ export type App = {
882
882
  orgId: Scalars['BigInt']['output'];
883
883
  /** OAuth-style redirect-URI allow-list for the portal handoff. A portal authorization code’s redirect_uri must match one of these by origin; empty disallows browser portal entry to this app. */
884
884
  redirectUris: Array<Scalars['String']['output']>;
885
- /** Whether replay logging is on: the realtime servers record every client input they accept for this app, searchable with inputLogSessions and readable with inputLogMessages, and billed as stored input logs. False (the default) records nothing ("No replication logging"). Set with updateApp (manage_apps); turning it on needs a funded org wallet or a billing-exempt org. */
885
+ /** Whether replay logging is on: the realtime servers record every client message they accept for this app, searchable with inputLogSessions and readable with inputLogMessages, and billed as stored input logs. False (the default) records nothing ("No replication logging"). Set with updateApp (manage_apps); turning it on needs a wallet that covers the projected cost of one retention period, or a billing-exempt org. */
886
886
  replayLoggingEnabled: Scalars['Boolean']['output'];
887
887
  /**
888
888
  * Reserved realtime (UDP) capacity in bytes/s for shared apps. Superseded by reservedUdpBytesPerSec.
@@ -1208,7 +1208,7 @@ export type AppUsageProjection = {
1208
1208
  daysElapsed: Scalars['Float']['output'];
1209
1209
  /** Per-app free monthly egress allowance in bytes (5 decimal GB). */
1210
1210
  freeAllowanceBytes: Scalars['String']['output'];
1211
- /** Stored input logs this calendar month so far, in byte-hours (input_log_storage_byte_hours, which has no free allowance). 0 when replay logging was off all month. */
1211
+ /** Stored input logs this calendar month so far, in byte-hours (input_log_storage_byte_hours, which has no free allowance). Recordings keep accruing after replay logging is turned off, until they age out at the end of the retention; 0 when nothing was held this month. */
1212
1212
  inputLogByteHours: Scalars['String']['output'];
1213
1213
  /** True when projected egress exceeds the free allowance, or null when insufficient data. */
1214
1214
  onTrackToExceed: Maybe<Scalars['Boolean']['output']>;
@@ -1236,7 +1236,7 @@ export type AppUsageSummary = {
1236
1236
  graphqlRecvBytes: Scalars['String']['output'];
1237
1237
  /** Total GraphQL bytes sent (string counter). */
1238
1238
  graphqlSendBytes: Scalars['String']['output'];
1239
- /** Stored input logs over the window, in byte-hours (string counter): the sum of the hourly samples, which is the quantity input_log_storage_byte_hours bills. Each recorded byte (InputLogMessage.sizeBytes) is held for the input log retention, one byte-hour per hour. 0 when replay logging was off. */
1239
+ /** Stored input logs over the window, in byte-hours (string counter): the sum of the hourly samples, which is the quantity input_log_storage_byte_hours bills. Each recorded byte (InputLogMessage.sizeBytes) is held for the input log retention, one byte-hour per hour, so recordings keep accruing byte-hours after replay logging is turned off, until they age out. 0 when nothing was held during the window. */
1240
1240
  inputLogByteHours: Scalars['String']['output'];
1241
1241
  /** Recorded input-log bytes the app holds now (string counter): the latest hourly sample, the bytes recorded within the trailing retention. 0 when replay logging has never been on. */
1242
1242
  inputLogRetainedBytes: Scalars['String']['output'];
@@ -4403,9 +4403,9 @@ export type InputLogSession = {
4403
4403
  byteCount: Scalars['BigInt']['output'];
4404
4404
  /** The datacenter that recorded the session, when known. */
4405
4405
  datacenter: Maybe<Scalars['String']['output']>;
4406
- /** How the session ended, as the client saw it: expired, revoked, reconnect or released. Null while it may still be running. */
4406
+ /** How the session ended: expired, revoked, reconnect or released, as the client saw it; logging_off, when the app turned replay logging off while the session was open; shutdown, when the realtime server recording it stopped (a deploy), after which the client reconnects elsewhere; paused, when recording paused because the app's logging setting could not be confirmed, which the client does not see (a later record reopens the session); or unrecorded, when its end was not recorded and it was closed an hour after its last record. Null while it may still be running. */
4407
4407
  endReason: Maybe<Scalars['String']['output']>;
4408
- /** When the session ended, if its end was recorded; null while it may still be running. */
4408
+ /** When the session ended; null while it may still be running. A session whose end was not recorded is closed at its last record once it has gone an hour without one. Later records reopen a session (endedAt returns to null). */
4409
4409
  endedAt: Maybe<Scalars['DateTime']['output']>;
4410
4410
  /** The game token the session ran on; pass it to inputLogMessages to read the inputs. */
4411
4411
  gameTokenId: Scalars['BigInt']['output'];
@@ -4415,6 +4415,8 @@ export type InputLogSession = {
4415
4415
  messageCount: Scalars['BigInt']['output'];
4416
4416
  /** The client message types the session contains, ascending. */
4417
4417
  messageTypes: Array<Scalars['Int']['output']>;
4418
+ /** Records the realtime servers numbered for this session that never reached the log: recording is best-effort, so an overloaded server or a broker outage can leave gaps, and this is how many. 0 when the recording is complete as far as it is known. A lost end of the recording cannot be counted (its session closes as unrecorded instead). Null for a session recorded before the realtime servers numbered their records. */
4419
+ missingRecords: Maybe<Scalars['BigInt']['output']>;
4418
4420
  /** When the first recorded input arrived. */
4419
4421
  startedAt: Scalars['DateTime']['output'];
4420
4422
  /** The user whose client sent the inputs. */
@@ -4928,7 +4930,7 @@ export type Mutation = {
4928
4930
  updateActor: Actor;
4929
4931
  /** Replaces an actor’s `publicState` and/or `privateState` blobs (fields omitted from `input` are left unchanged). OWNER-EXCLUSIVE: only the actor’s owner may write (throws Unauthorized otherwise). Game-plane: requires an app token for the actor’s app (a session token or another app’s token is answered NotFound). `uuid` is the 32-character ASCII actor id; blobs are base64-encoded binary. */
4930
4932
  updateActorState: Actor;
4931
- /** Update mutable fields of an existing app (name, description, visibility, status, metadata, wildernessWritesOpen, replayLoggingEnabled); only fields present in the input are changed. Turning replayLoggingEnabled on is refused with INPUT_LOG_FUNDS_NEEDED unless the org's wallet has a spendable balance or the org is billing-exempt. Requires the 'manage_apps' permission on the app (resolved via its org); super admins bypass. Use this to publish (status=LIVE), change visibility, or restore an archived app (status back to DRAFT/LIVE). Throws if the app id does not exist. */
4933
+ /** Update mutable fields of an existing app (name, description, visibility, status, metadata, wildernessWritesOpen, replayLoggingEnabled); only fields present in the input are changed. Turning replayLoggingEnabled on is refused with INPUT_LOG_FUNDS_NEEDED unless the org's wallet can spend at least the projected cost of keeping one retention period of the app's recent traffic (extensions.requiredMicrousd and spendableMicrousd say how much), or the org is billing-exempt; recordings already kept go on billing after it is turned off, until they age out. Requires the 'manage_apps' permission on the app (resolved via its org); super admins bypass. Use this to publish (status=LIVE), change visibility, or restore an archived app (status back to DRAFT/LIVE). Throws if the app id does not exist. */
4932
4934
  updateApp: App;
4933
4935
  /** Updates an avatar’s mutable fields (currently `name`) and returns it. OWNER-EXCLUSIVE: only the avatar’s owner may call this (throws Unauthorized otherwise). Requires a valid game token. To change state blobs use `updateAvatarState`. */
4934
4936
  updateAvatar: Avatar;
@@ -6787,7 +6789,7 @@ export type Query = {
6787
6789
  hostedGamePublishes: Array<HostedGamePublish>;
6788
6790
  /** The LIVE and LISTED hosted third-party games, for the Overworld lobby and the marketplace. PUBLIC. Empty when the tier does not host third-party games. Operators see every game with allHostedGames. */
6789
6791
  hostedGames: Array<HostedGame>;
6790
- /** The recorded inputs of one session, oldest first, each exactly as the client sent it (without its authentication tail). Read from the input log by the offset ranges its index holds, so inputs past the published retention are gone even while the session is still listed. With 'manage_apps' on the app you may read any session; anyone else may read only their own, and any other session answers NOT_FOUND. Game plane: send the app-scoped token for this app to the app's datacenter. Page with `first` (default 50, max 200) and `after`; a page can hold fewer than `first` inputs when it stopped at its time or scan limit, so keep paging while hasNextPage is true. INPUT_LOG_UNAVAILABLE when input logging is not configured on this deployment, and also, retryable with the same cursor, when the log cannot be read right now. */
6792
+ /** The recorded inputs of one session, oldest first, each exactly as the client sent it (without its authentication tail). Read from the input log by the offset ranges its index holds, so inputs past the published retention are gone even while the session is still listed. With 'manage_apps' on the app you may read any session; anyone else may read only their own, and any other session answers NOT_FOUND. Game plane: send the app-scoped token for this app to the app's datacenter. Page with `first` (default 50, max 200) and `after`, the previous page's endCursor for this same session (a cursor from another session is BAD_USER_INPUT); a page can hold fewer than `first` inputs, or none, when it stopped at its time or scan limit, so keep paging while hasNextPage is true. An operation may select this field only once. A server runs one read per user (two per app and per org) at a time; a read over that share waits for the earlier one within its own few-second deadline, and a read whose request is closed stops. Errors: INPUT_LOG_UNAVAILABLE when input logging is not configured on this deployment, or this server may not read its log (do not retry); INPUT_LOG_RATE_LIMITED (429) when the wait for an earlier read ran out or left too little time to read, and INPUT_LOG_TEMPORARILY_UNAVAILABLE (503) when the log cannot be read just now, both with extensions.retryable: retry shortly with the same cursor. A read whose request closed stops as INPUT_LOG_READ_ABORTED (499), which nobody receives. */
6791
6793
  inputLogMessages: InputLogMessageConnection;
6792
6794
  /** Recorded client sessions of an app with replay logging on (App.replayLoggingEnabled), newest first. With 'manage_apps' on the app you see every session and may filter by userId; anyone else sees only their own, and asking for another user's is FORBIDDEN. Game plane: send the app-scoped token for this app to the app's datacenter. Page with `first` (default 50, max 200) and `after`. INPUT_LOG_UNAVAILABLE when input logging is not configured on this deployment. */
6793
6795
  inputLogSessions: InputLogSessionConnection;
@@ -8630,7 +8632,7 @@ export type UpdateAppInput = {
8630
8632
  metadata?: InputMaybe<Scalars['String']['input']>;
8631
8633
  /** New display name (1-256 chars). Omit to leave unchanged. */
8632
8634
  name?: InputMaybe<Scalars['String']['input']>;
8633
- /** Turn replay logging on (true) or off (false): record every client input the realtime servers accept for this app, kept for the input log's published retention and billed as stored input logs (input_log_storage_byte_hours, no free allowance). Turning it ON is refused with INPUT_LOG_FUNDS_NEEDED unless the org's wallet has a spendable balance or the org is billing-exempt; turning it off always succeeds. Recording starts and stops within about 15 seconds. Omit to leave unchanged. */
8635
+ /** Turn replay logging on (true) or off (false): record every client message the realtime servers accept for this app (keep-alives included), kept for the input log's published retention and billed as stored input logs (input_log_storage_byte_hours, no free allowance). Turning it ON is refused with INPUT_LOG_FUNDS_NEEDED unless the org's wallet can spend at least the projected cost of keeping one retention period of the app's recent traffic, or the org is billing-exempt; turning it off always succeeds, and what was already recorded goes on billing until it ages out. Recording starts and stops within about 15 seconds. Omit to leave unchanged. */
8634
8636
  replayLoggingEnabled?: InputMaybe<Scalars['Boolean']['input']>;
8635
8637
  /** New lifecycle status; set LIVE to publish, or DRAFT/LIVE to restore an archived app. Omit to leave unchanged. */
8636
8638
  status?: InputMaybe<AppStatus>;