@crowdedkingdoms/crowdyjs 18.19.0 → 18.20.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/README.md CHANGED
@@ -12,19 +12,23 @@ gameplay, minted per app through `client.portal`.
12
12
 
13
13
  Start here:
14
14
 
15
- - [SDK guide](https://docs.crowdedkingdoms.com/crowdyjs/readme) — the canonical
15
+ - [SDK guide](https://docs.dev.crowdedkingdoms.com/crowdyjs/readme) — the canonical
16
16
  walkthrough of everything in this README.
17
- - [Build a game](https://docs.crowdedkingdoms.com/build-a-game/intro) — a
17
+ - [Build a game](https://docs.dev.crowdedkingdoms.com/build-a-game/intro) — a
18
18
  hands-on tutorial from sign-in to a playable voxel game.
19
- - [Platform overview](https://docs.crowdedkingdoms.com/overview/client-workflow)
19
+ - [Platform overview](https://docs.dev.crowdedkingdoms.com/overview/client-workflow)
20
20
  — how a client session flows through the platform.
21
21
 
22
22
  ## Install
23
23
 
24
24
  ```bash
25
- npm install @crowdedkingdoms/crowdyjs
25
+ npm install @crowdedkingdoms/crowdyjs@dev
26
26
  ```
27
27
 
28
+ This README is the `dev` branch's: it documents the release on the `dev` dist-tag
29
+ (`npm view @crowdedkingdoms/crowdyjs dist-tags`), and its links go to the dev docs site. A
30
+ plain `npm install` resolves `latest`, the production release, which has no `client.exec`.
31
+
28
32
  **Server compatibility.** A release selects fields the Game API it was built against has,
29
33
  so it needs that Game API or newer; promote the Game API before the SDK on each tier.
30
34
 
@@ -196,8 +200,8 @@ Notes:
196
200
  - Use `client.auth.setToken(token)` to seed a token externally (e.g. when
197
201
  restoring auth from a non-default storage).
198
202
 
199
- Deeper reading: [Portals & app-scoped tokens](https://docs.crowdedkingdoms.com/management-api/portals-and-app-tokens)
200
- and [Sign in](https://docs.crowdedkingdoms.com/management-api/authentication).
203
+ Deeper reading: [Portals & app-scoped tokens](https://docs.dev.crowdedkingdoms.com/management-api/portals-and-app-tokens)
204
+ and [Sign in](https://docs.dev.crowdedkingdoms.com/management-api/authentication).
201
205
 
202
206
  ### Org-managed players
203
207
 
@@ -394,7 +398,7 @@ query AppForRouting($appId: BigInt!) {
394
398
  `CrowdyClient` with `httpUrl: gameApiUrl` (and the matching `wsUrl`) holding
395
399
  that app's app-scoped token, then drive gameplay through that client. Apps
396
400
  with no `gameApiUrl` keep working against the default `httpUrl` you
397
- configured. See [Loading an app's Game API](https://docs.crowdedkingdoms.com/crowdyjs/shared-environment-routing).
401
+ configured. See [Loading an app's Game API](https://docs.dev.crowdedkingdoms.com/crowdyjs/shared-environment-routing).
398
402
 
399
403
  ## Realtime notifications
400
404
 
@@ -552,7 +556,14 @@ The plain `sendActorUpdate`, `sendVoxelUpdate`, `sendAudioPacket`,
552
556
  `sendTextPacket`, and `sendClientEvent` methods return the GraphQL mutation
553
557
  result immediately. The `AndWait` variants allocate a `sequenceNumber` when
554
558
  one is missing and wait for either a matching notification or
555
- `GenericErrorResponse`. `sendChannelMessage` broadcasts an opaque payload on a
559
+ `GenericErrorResponse`, timing the wait from when the send was accepted. The
560
+ first awaited send of each session (the first connect, a reconnect, or a move to
561
+ another instance) is retried once with a fresh `sequenceNumber` if it is refused
562
+ `UNAUTHORIZED` or, for the actor and voxel updates the server echoes, if it times
563
+ out (`UDP_SEQUENCE_TIMEOUT`), which is how a realtime server older than v0.42.0
564
+ answers a session's first message. A send whose `sequenceNumber` you chose is
565
+ not retried, and an audio, text or event send's timeout (its normal success) is
566
+ never retried. `sendChannelMessage` broadcasts an opaque payload on a
556
567
  channel. `sendRangedChannelMessage` publishes on a channel too, but only members
557
568
  with a live actor within `maxDistance` chunks (straight-line distance between
558
569
  chunk coordinates) of `chunk` receive it; they get the same `channelMessage`
@@ -820,7 +831,7 @@ reach your bundle. Reads are synchronous snapshots and writes happen on
820
831
  WebSocket events (not `requestAnimationFrame`), so render loops read freely
821
832
  and a backgrounded tab keeps ingesting updates; pass `workerTicker()` to also
822
833
  keep timer-driven sends at full rate while hidden. See the
823
- [World Stores guide](https://docs.crowdedkingdoms.com/crowdyjs/stores).
834
+ [World Stores guide](https://docs.dev.crowdedkingdoms.com/crowdyjs/stores).
824
835
 
825
836
  ## Game Kit
826
837
 
@@ -898,7 +909,7 @@ The protocol is held to ck-exec's golden record of the starters
898
909
  (`test/unit/fixtures/kit-protocol.json`): every module call and topic in it is replayed in
899
910
  `test/unit/game-kit.test.mjs`. The blueprints, `kit.deploy`, the engines and the model-backed
900
911
  helpers went with the game model in 18.0.0; the hub modules replace them. See the
901
- [Game Kit docs](https://docs.crowdedkingdoms.com/game-kit/intro) and
912
+ [Game Kit docs](https://docs.dev.crowdedkingdoms.com/game-kit/intro) and
902
913
  [MIGRATION.md](MIGRATION.md).
903
914
 
904
915
  ## ck-exec
@@ -962,7 +973,7 @@ Integers above 2^53 decode as `number` and lose precision unless you pass
962
973
  `{ decode: { useBigInt64: true } }`. A call's whole numbers outside 32 bits (a timestamp in
963
974
  milliseconds, a player id) go out as 64-bit integers (18.11.0; before, as floats, which a
964
975
  hub's integer fields refuse). The wire format is in the
965
- [ck-exec docs](https://docs.crowdedkingdoms.com/exec/intro).
976
+ [ck-exec docs](https://docs.dev.crowdedkingdoms.com/exec/intro).
966
977
 
967
978
  ### A mod's CLIENT half
968
979
 
@@ -1033,7 +1044,7 @@ counts what was dropped); `invoke` sends at most 256 KiB each way and is refused
1033
1044
  module is not running. `NOT_FOUND` holds a CLIENT half back 15 s and `RATE_LIMITED` 60 s. The
1034
1045
  grid's owner is asked about their own CLIENT halves like anyone; answer yes in `confirm` when
1035
1046
  `prompt.authorId` is the player. See
1036
- [CLIENT halves](https://docs.crowdedkingdoms.com/exec/client-halves).
1047
+ [CLIENT halves](https://docs.dev.crowdedkingdoms.com/exec/client-halves).
1037
1048
 
1038
1049
  ## Hosting a game on Crowdy Games
1039
1050
 
@@ -1152,8 +1163,8 @@ CLIENT project needs ck-api `v2.25.1` or later. A CLIENT crate still on the lega
1152
1163
  `mountCrowdyStudio` and the controller take `mods: client.exec`. Crowdy Studio runs nothing
1153
1164
  on legacy player compute since 18.0.0.
1154
1165
 
1155
- See [CLIENT halves](https://docs.crowdedkingdoms.com/exec/client-halves), [mods](https://docs.crowdedkingdoms.com/exec/mods)
1156
- and [Embed Crowdy Studio in your game](https://docs.crowdedkingdoms.com/crowdyjs/crowdy-studio-embed).
1166
+ See [CLIENT halves](https://docs.dev.crowdedkingdoms.com/exec/client-halves), [mods](https://docs.dev.crowdedkingdoms.com/exec/mods)
1167
+ and [Embed Crowdy Studio in your game](https://docs.dev.crowdedkingdoms.com/crowdyjs/crowdy-studio-embed).
1157
1168
 
1158
1169
  ### The Studio agent (DeepSeek Harness in the browser)
1159
1170
 
@@ -1344,7 +1355,7 @@ no other repositories and no network access required:
1344
1355
  `src/generated/graphql-schema.ts`, the generated schema types.
1345
1356
 
1346
1357
  Schema refresh is explicit, from the published
1347
- [SDL](https://docs.crowdedkingdoms.com/schema/game-api.graphql):
1358
+ [SDL](https://docs.dev.crowdedkingdoms.com/schema/game-api.graphql):
1348
1359
 
1349
1360
  ```bash
1350
1361
  npm run schema:sync:prod
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * GENERATED — do not edit. The operator tooling's
3
- * `sync-client-origins.mjs --write --tier prod` writes this file, and
3
+ * `sync-client-origins.mjs --write --tier dev` writes this file, and
4
4
  * `check-sdk-default-origin.mjs` refuses it when it names the wrong tier or a
5
5
  * host that declaration does not carry. Regenerate; never hand-edit.
6
6
  *
@@ -10,14 +10,14 @@
10
10
  * than no default at all. Every fallback this SDK ever removed named a host that
11
11
  * was already dead. A generated file plus a gate is what makes this one different.
12
12
  *
13
- * Source: the operator's per-tier public CK API origin declaration, tier 'prod'
13
+ * Source: the operator's per-tier public CK API origin declaration, tier 'dev'
14
14
  */
15
15
  /** The tier this build of the SDK is published for. */
16
- export declare const CROWDY_DEFAULT_TIER = "prod";
16
+ export declare const CROWDY_DEFAULT_TIER = "dev";
17
17
  /** The public CK API origin for that tier. */
18
- export declare const CROWDY_DEFAULT_HTTP_ORIGIN = "https://ck.prod.crowdedkingdoms.com";
18
+ export declare const CROWDY_DEFAULT_HTTP_ORIGIN = "https://ck.dev.crowdedkingdoms.com";
19
19
  /** The same host over WebSocket. A scheme is composed; a hostname is looked up. */
20
- export declare const CROWDY_DEFAULT_WS_ORIGIN = "wss://ck.prod.crowdedkingdoms.com";
20
+ export declare const CROWDY_DEFAULT_WS_ORIGIN = "wss://ck.dev.crowdedkingdoms.com";
21
21
  /** The bare hostname, for callers that need to compare rather than dial. */
22
- export declare const CROWDY_DEFAULT_HOST = "ck.prod.crowdedkingdoms.com";
22
+ export declare const CROWDY_DEFAULT_HOST = "ck.dev.crowdedkingdoms.com";
23
23
  //# sourceMappingURL=default-origin.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"default-origin.d.ts","sourceRoot":"","sources":["../src/default-origin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,uDAAuD;AACvD,eAAO,MAAM,mBAAmB,SAAS,CAAC;AAE1C,8CAA8C;AAC9C,eAAO,MAAM,0BAA0B,wCAAwC,CAAC;AAEhF,mFAAmF;AACnF,eAAO,MAAM,wBAAwB,sCAAsC,CAAC;AAE5E,4EAA4E;AAC5E,eAAO,MAAM,mBAAmB,gCAAgC,CAAC"}
1
+ {"version":3,"file":"default-origin.d.ts","sourceRoot":"","sources":["../src/default-origin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,uDAAuD;AACvD,eAAO,MAAM,mBAAmB,QAAQ,CAAC;AAEzC,8CAA8C;AAC9C,eAAO,MAAM,0BAA0B,uCAAuC,CAAC;AAE/E,mFAAmF;AACnF,eAAO,MAAM,wBAAwB,qCAAqC,CAAC;AAE3E,4EAA4E;AAC5E,eAAO,MAAM,mBAAmB,+BAA+B,CAAC"}
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * GENERATED — do not edit. The operator tooling's
3
- * `sync-client-origins.mjs --write --tier prod` writes this file, and
3
+ * `sync-client-origins.mjs --write --tier dev` writes this file, and
4
4
  * `check-sdk-default-origin.mjs` refuses it when it names the wrong tier or a
5
5
  * host that declaration does not carry. Regenerate; never hand-edit.
6
6
  *
@@ -10,13 +10,13 @@
10
10
  * than no default at all. Every fallback this SDK ever removed named a host that
11
11
  * was already dead. A generated file plus a gate is what makes this one different.
12
12
  *
13
- * Source: the operator's per-tier public CK API origin declaration, tier 'prod'
13
+ * Source: the operator's per-tier public CK API origin declaration, tier 'dev'
14
14
  */
15
15
  /** The tier this build of the SDK is published for. */
16
- export const CROWDY_DEFAULT_TIER = 'prod';
16
+ export const CROWDY_DEFAULT_TIER = 'dev';
17
17
  /** The public CK API origin for that tier. */
18
- export const CROWDY_DEFAULT_HTTP_ORIGIN = 'https://ck.prod.crowdedkingdoms.com';
18
+ export const CROWDY_DEFAULT_HTTP_ORIGIN = 'https://ck.dev.crowdedkingdoms.com';
19
19
  /** The same host over WebSocket. A scheme is composed; a hostname is looked up. */
20
- export const CROWDY_DEFAULT_WS_ORIGIN = 'wss://ck.prod.crowdedkingdoms.com';
20
+ export const CROWDY_DEFAULT_WS_ORIGIN = 'wss://ck.dev.crowdedkingdoms.com';
21
21
  /** The bare hostname, for callers that need to compare rather than dial. */
22
- export const CROWDY_DEFAULT_HOST = 'ck.prod.crowdedkingdoms.com';
22
+ export const CROWDY_DEFAULT_HOST = 'ck.dev.crowdedkingdoms.com';
@@ -33,10 +33,13 @@ import { type VideoCodec } from '../media/video-frames.js';
33
33
  *
34
34
  * Spatial caveats: a `sequenceNumber` is a uint8 (0-255) used for
35
35
  * **correlation only** — not an idempotency key, and the server does not dedupe
36
- * replays. The **first** spatial message to a brand-new chunk may be dropped
37
- * server-side while grid permissions load, so clients should re-send (the
38
- * two-client tests register twice for this reason). To receive any
39
- * `...AndWait` echo or notification you must have an active {@link subscribe}.
36
+ * replays. The game server holds a session's first messages, and the first
37
+ * message into a newly entered region, until the permissions they need have
38
+ * loaded (realtime server v0.42.0 and later); an older server dropped or
39
+ * refused them. Against one, the SDK retries the first awaited send of each
40
+ * session once (see {@link sendActorUpdateAndWait}), but a plain `send*` may
41
+ * still be lost. To receive any `...AndWait` echo or notification you must have
42
+ * an active {@link subscribe}.
40
43
  */
41
44
  export declare class UdpAPI {
42
45
  private gql;
@@ -50,6 +53,8 @@ export declare class UdpAPI {
50
53
  private waitForGameplayTokenRefresh?;
51
54
  private readonly sequences;
52
55
  private readonly inFlightActorUpdates;
56
+ /** The session epoch of the last awaited send that was answered. */
57
+ private confirmedEpoch;
53
58
  constructor(gql: GraphQLClient, subs: SubscriptionManager, metrics?: RealtimeMetrics | undefined,
54
59
  /**
55
60
  * When set, every UDP send waits for this promise (a gameplay-token
@@ -159,8 +164,22 @@ export declare class UdpAPI {
159
164
  * @param input - {@link ActorUpdateRequestInput} (see {@link sendActorUpdate}
160
165
  * for field units/encoding). Omit `sequenceNumber` to let the SDK allocate
161
166
  * one.
162
- * @param options - `timeoutMs`: how long to wait for the echo, in
163
- * milliseconds; defaults to the client's realtime `waitTimeoutMs` (5000).
167
+ * Every `send*AndWait` behaves as follows:
168
+ * - `timeoutMs` counts from when the send was accepted, not from the call, so
169
+ * a slow send leg (the first send opening the proxy session, the binary
170
+ * relay finishing its handshake) does not shorten the wait for the echo.
171
+ * - The first awaited send of a new session (the first connect, a reconnect,
172
+ * a move to another instance) is retried once with a fresh sequence number
173
+ * if it is refused `UNAUTHORIZED` or, for the actor and voxel updates the
174
+ * server echoes, if its echo times out (`UDP_SEQUENCE_TIMEOUT`), which is
175
+ * how a game server older than v0.42.0 answers a session's first messages;
176
+ * the resolved notification then carries the retry's sequence number. A
177
+ * send whose `sequenceNumber` you chose is not retried, and neither is the
178
+ * timeout of an audio, text or event send, which is how those succeed.
179
+ *
180
+ * @param options - `timeoutMs`: how long to wait for the echo once the send
181
+ * is accepted, in milliseconds; defaults to the client's realtime
182
+ * `waitTimeoutMs` (5000).
164
183
  * @returns The correlated {@link SpatialNotification} (your own
165
184
  * `ActorUpdateNotification` self-echo).
166
185
  * @throws {CrowdyGraphQLError} if the underlying send is rejected.
@@ -168,7 +187,8 @@ export declare class UdpAPI {
168
187
  * request timeout.
169
188
  * @throws {CrowdyRealtimeError} if no echo arrives within `timeoutMs`
170
189
  * (`code === 'UDP_SEQUENCE_TIMEOUT'`, retryable), or the server returns a
171
- * matching `GenericErrorResponse` (its `code` is the server's `errorCode`).
190
+ * matching `GenericErrorResponse` (its `code` is the server's `errorCode`),
191
+ * after the one retry described above when it applies.
172
192
  * @example
173
193
  * ```ts
174
194
  * const unsubscribe = client.udp.subscribe({}, appId); // echoes need an open WS
@@ -588,6 +608,26 @@ export declare class UdpAPI {
588
608
  * ```
589
609
  */
590
610
  subscribe(handlers: UdpNotificationHandlers, appId: string): () => void;
611
+ /**
612
+ * What every `send*AndWait` does.
613
+ *
614
+ * The wait is registered before the send, so an echo that beats the send's
615
+ * own response is not missed, and its clock starts once the send has been
616
+ * accepted, so a lazy proxy-session open or the relay's handshake on the send
617
+ * leg is not taken from the echo's window.
618
+ *
619
+ * The first awaited send of a new session (the first connect, a reconnect, or
620
+ * a move to another instance) is retried once, with a fresh sequence number,
621
+ * when it is refused `UNAUTHORIZED` or, for a send the server echoes, when
622
+ * its echo times out: a game server from before the one that holds a
623
+ * session's first messages refuses or drops them while the session's
624
+ * permissions load. A send the server does not echo (audio, text, a client
625
+ * event) succeeds by timing out, so its timeout is never retried. Only when
626
+ * the SDK chose the sequence number, because the retry needs one of its own
627
+ * and a caller who chose theirs is correlating by it.
628
+ */
629
+ private sendAndWait;
630
+ private sendOnceAndWait;
591
631
  private withSequence;
592
632
  }
593
633
  //# sourceMappingURL=udp.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"udp.d.ts","sourceRoot":"","sources":["../../src/domains/udp.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"udp.d.ts","sourceRoot":"","sources":["../../src/domains/udp.ts"],"names":[],"mappings":"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"}
@@ -33,10 +33,13 @@ import { encodeBase64 } from '../utils.js';
33
33
  *
34
34
  * Spatial caveats: a `sequenceNumber` is a uint8 (0-255) used for
35
35
  * **correlation only** — not an idempotency key, and the server does not dedupe
36
- * replays. The **first** spatial message to a brand-new chunk may be dropped
37
- * server-side while grid permissions load, so clients should re-send (the
38
- * two-client tests register twice for this reason). To receive any
39
- * `...AndWait` echo or notification you must have an active {@link subscribe}.
36
+ * replays. The game server holds a session's first messages, and the first
37
+ * message into a newly entered region, until the permissions they need have
38
+ * loaded (realtime server v0.42.0 and later); an older server dropped or
39
+ * refused them. Against one, the SDK retries the first awaited send of each
40
+ * session once (see {@link sendActorUpdateAndWait}), but a plain `send*` may
41
+ * still be lost. To receive any `...AndWait` echo or notification you must have
42
+ * an active {@link subscribe}.
40
43
  */
41
44
  export class UdpAPI {
42
45
  constructor(gql, subs, metrics,
@@ -52,6 +55,8 @@ export class UdpAPI {
52
55
  this.waitForGameplayTokenRefresh = waitForGameplayTokenRefresh;
53
56
  this.sequences = new SequenceAllocator();
54
57
  this.inFlightActorUpdates = new Set();
58
+ /** The session epoch of the last awaited send that was answered. */
59
+ this.confirmedEpoch = -1;
55
60
  }
56
61
  /**
57
62
  * Resolves when every in-flight {@link sendActorUpdate} has settled.
@@ -261,8 +266,22 @@ export class UdpAPI {
261
266
  * @param input - {@link ActorUpdateRequestInput} (see {@link sendActorUpdate}
262
267
  * for field units/encoding). Omit `sequenceNumber` to let the SDK allocate
263
268
  * one.
264
- * @param options - `timeoutMs`: how long to wait for the echo, in
265
- * milliseconds; defaults to the client's realtime `waitTimeoutMs` (5000).
269
+ * Every `send*AndWait` behaves as follows:
270
+ * - `timeoutMs` counts from when the send was accepted, not from the call, so
271
+ * a slow send leg (the first send opening the proxy session, the binary
272
+ * relay finishing its handshake) does not shorten the wait for the echo.
273
+ * - The first awaited send of a new session (the first connect, a reconnect,
274
+ * a move to another instance) is retried once with a fresh sequence number
275
+ * if it is refused `UNAUTHORIZED` or, for the actor and voxel updates the
276
+ * server echoes, if its echo times out (`UDP_SEQUENCE_TIMEOUT`), which is
277
+ * how a game server older than v0.42.0 answers a session's first messages;
278
+ * the resolved notification then carries the retry's sequence number. A
279
+ * send whose `sequenceNumber` you chose is not retried, and neither is the
280
+ * timeout of an audio, text or event send, which is how those succeed.
281
+ *
282
+ * @param options - `timeoutMs`: how long to wait for the echo once the send
283
+ * is accepted, in milliseconds; defaults to the client's realtime
284
+ * `waitTimeoutMs` (5000).
266
285
  * @returns The correlated {@link SpatialNotification} (your own
267
286
  * `ActorUpdateNotification` self-echo).
268
287
  * @throws {CrowdyGraphQLError} if the underlying send is rejected.
@@ -270,7 +289,8 @@ export class UdpAPI {
270
289
  * request timeout.
271
290
  * @throws {CrowdyRealtimeError} if no echo arrives within `timeoutMs`
272
291
  * (`code === 'UDP_SEQUENCE_TIMEOUT'`, retryable), or the server returns a
273
- * matching `GenericErrorResponse` (its `code` is the server's `errorCode`).
292
+ * matching `GenericErrorResponse` (its `code` is the server's `errorCode`),
293
+ * after the one retry described above when it applies.
274
294
  * @example
275
295
  * ```ts
276
296
  * const unsubscribe = client.udp.subscribe({}, appId); // echoes need an open WS
@@ -285,13 +305,7 @@ export class UdpAPI {
285
305
  * ```
286
306
  */
287
307
  async sendActorUpdateAndWait(input, options = {}) {
288
- const request = this.withSequence(input);
289
- const wait = this.subs.waitForSequence(request.sequenceNumber, options.timeoutMs);
290
- await this.sendActorUpdate(request);
291
- // Nothing can be echoed that has not left: the bundled relay path holds a
292
- // send for up to bundleWindowMs, so put it on the wire before waiting.
293
- this.subs.flushSends();
294
- return wait;
308
+ return this.sendAndWait(input, options.timeoutMs, true, (request) => this.sendActorUpdate(request));
295
309
  }
296
310
  /**
297
311
  * Send a single voxel (block) update for spatial replication to nearby
@@ -358,13 +372,7 @@ export class UdpAPI {
358
372
  * (its `code` is the server's `errorCode`).
359
373
  */
360
374
  async sendVoxelUpdateAndWait(input, options = {}) {
361
- const request = this.withSequence(input);
362
- const wait = this.subs.waitForSequence(request.sequenceNumber, options.timeoutMs);
363
- await this.sendVoxelUpdate(request);
364
- // Nothing can be echoed that has not left: the bundled relay path holds a
365
- // send for up to bundleWindowMs, so put it on the wire before waiting.
366
- this.subs.flushSends();
367
- return wait;
375
+ return this.sendAndWait(input, options.timeoutMs, true, (request) => this.sendVoxelUpdate(request));
368
376
  }
369
377
  /**
370
378
  * Send a spatial voice/audio packet, fanned out to nearby actors as a
@@ -423,13 +431,7 @@ export class UdpAPI {
423
431
  * `errorCode`, e.g. `UNAUTHORIZED` for a missing voice permission).
424
432
  */
425
433
  async sendAudioPacketAndWait(input, options = {}) {
426
- const request = this.withSequence(input);
427
- const wait = this.subs.waitForSequence(request.sequenceNumber, options.timeoutMs);
428
- await this.sendAudioPacket(request);
429
- // Nothing can be echoed that has not left: the bundled relay path holds a
430
- // send for up to bundleWindowMs, so put it on the wire before waiting.
431
- this.subs.flushSends();
432
- return wait;
434
+ return this.sendAndWait(input, options.timeoutMs, false, (request) => this.sendAudioPacket(request));
433
435
  }
434
436
  /**
435
437
  * Send ONE webcam video fragment, fanned out to nearby actors as a
@@ -540,13 +542,7 @@ export class UdpAPI {
540
542
  * `errorCode`).
541
543
  */
542
544
  async sendTextPacketAndWait(input, options = {}) {
543
- const request = this.withSequence(input);
544
- const wait = this.subs.waitForSequence(request.sequenceNumber, options.timeoutMs);
545
- await this.sendTextPacket(request);
546
- // Nothing can be echoed that has not left: the bundled relay path holds a
547
- // send for up to bundleWindowMs, so put it on the wire before waiting.
548
- this.subs.flushSends();
549
- return wait;
545
+ return this.sendAndWait(input, options.timeoutMs, false, (request) => this.sendTextPacket(request));
550
546
  }
551
547
  /**
552
548
  * Send a custom, app-defined client event for spatial replication to nearby
@@ -603,13 +599,7 @@ export class UdpAPI {
603
599
  * `errorCode`).
604
600
  */
605
601
  async sendClientEventAndWait(input, options = {}) {
606
- const request = this.withSequence(input);
607
- const wait = this.subs.waitForSequence(request.sequenceNumber, options.timeoutMs);
608
- await this.sendClientEvent(request);
609
- // Nothing can be echoed that has not left: the bundled relay path holds a
610
- // send for up to bundleWindowMs, so put it on the wire before waiting.
611
- this.subs.flushSends();
612
- return wait;
602
+ return this.sendAndWait(input, options.timeoutMs, false, (request) => this.sendClientEvent(request));
613
603
  }
614
604
  /**
615
605
  * Send a direct actor-to-actor message, delivered only to the actor whose
@@ -824,6 +814,59 @@ export class UdpAPI {
824
814
  subscribe(handlers, appId) {
825
815
  return this.subs.subscribe(handlers, appId);
826
816
  }
817
+ /**
818
+ * What every `send*AndWait` does.
819
+ *
820
+ * The wait is registered before the send, so an echo that beats the send's
821
+ * own response is not missed, and its clock starts once the send has been
822
+ * accepted, so a lazy proxy-session open or the relay's handshake on the send
823
+ * leg is not taken from the echo's window.
824
+ *
825
+ * The first awaited send of a new session (the first connect, a reconnect, or
826
+ * a move to another instance) is retried once, with a fresh sequence number,
827
+ * when it is refused `UNAUTHORIZED` or, for a send the server echoes, when
828
+ * its echo times out: a game server from before the one that holds a
829
+ * session's first messages refuses or drops them while the session's
830
+ * permissions load. A send the server does not echo (audio, text, a client
831
+ * event) succeeds by timing out, so its timeout is never retried. Only when
832
+ * the SDK chose the sequence number, because the retry needs one of its own
833
+ * and a caller who chose theirs is correlating by it.
834
+ */
835
+ async sendAndWait(input, timeoutMs, echoed, send) {
836
+ const epoch = this.subs.sessionEpoch();
837
+ const retryable = epoch !== this.confirmedEpoch && input.sequenceNumber == null;
838
+ try {
839
+ const echo = await this.sendOnceAndWait(this.withSequence(input), timeoutMs, send).catch((error) => {
840
+ if (!retryable || !isFirstSendFailure(error, echoed))
841
+ throw error;
842
+ return this.sendOnceAndWait(this.withSequence({ ...input, sequenceNumber: undefined }), timeoutMs, send);
843
+ });
844
+ this.confirmedEpoch = epoch;
845
+ return echo;
846
+ }
847
+ catch (error) {
848
+ // A send the server does not echo succeeds by timing out, and that
849
+ // answers for the session as well as an echo would.
850
+ if (!echoed && isTimeout(error))
851
+ this.confirmedEpoch = epoch;
852
+ throw error;
853
+ }
854
+ }
855
+ async sendOnceAndWait(request, timeoutMs, send) {
856
+ const wait = this.subs.expectSequence(request.sequenceNumber, timeoutMs);
857
+ try {
858
+ await send(request);
859
+ }
860
+ catch (error) {
861
+ wait.cancel();
862
+ throw error;
863
+ }
864
+ // Nothing can be echoed that has not left: the bundled relay path holds a
865
+ // send for up to bundleWindowMs, so put it on the wire before waiting.
866
+ this.subs.flushSends();
867
+ wait.start();
868
+ return wait.promise;
869
+ }
827
870
  withSequence(input) {
828
871
  return {
829
872
  ...input,
@@ -831,3 +874,11 @@ export class UdpAPI {
831
874
  };
832
875
  }
833
876
  }
877
+ /** How a session's first message failed on a game server that did not hold it. */
878
+ function isFirstSendFailure(error, echoed) {
879
+ return (error instanceof CrowdyRealtimeError &&
880
+ (error.code === 'UNAUTHORIZED' || (echoed && isTimeout(error))));
881
+ }
882
+ function isTimeout(error) {
883
+ return error instanceof CrowdyRealtimeError && error.code === 'UDP_SEQUENCE_TIMEOUT';
884
+ }
@@ -8951,7 +8951,7 @@ export declare enum UdpErrorCode {
8951
8951
  PasswordTooShort = "PASSWORD_TOO_SHORT",
8952
8952
  /** The app-scoped gameplay token has expired. Refresh it (same app, via refreshAppToken) before it lapses, or re-portal through the Overworld for a fresh token, then re-authorize the realtime session. */
8953
8953
  TokenExpired = "TOKEN_EXPIRED",
8954
- /** The caller lacks the runtime/grid permission required for this action. Grid permissions can load asynchronously, so the first message to a newly entered region may transiently return this — retry shortly. */
8954
+ /** The caller lacks the runtime/grid permission required for this action. A message sent right after connecting or entering a new region waits for that permission to load rather than being refused, so this means the permission is missing or could not be confirmed in time; after a move or a new session, one retry is enough to tell the two apart. */
8955
8955
  Unauthorized = "UNAUTHORIZED",
8956
8956
  /** Unspecified server error (1). Retry; if it persists, report it. */
8957
8957
  UnknownError = "UNKNOWN_ERROR",