@crowdedkingdoms/crowdy-dsh 0.4.12 → 0.4.13-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.
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "package": "@crowdedkingdoms/crowdy-dsh",
3
- "version": "0.4.12",
4
- "builtAt": "2026-10-24T14:23:55.299Z",
3
+ "version": "0.4.13",
4
+ "builtAt": "2026-10-24T21:54:31.702Z",
5
5
  "source": {
6
6
  "repository": "CrowdedKingdoms/crowdy-dsh",
7
- "commit": "da2e4b36025c6f7944bbcf0ed799fabf05c5c044",
7
+ "commit": "a2422f393aebc79753ac80845ed0cee1cb05974e",
8
8
  "dirty": false
9
9
  },
10
10
  "upstream": {
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "crowdyjs": {
17
17
  "name": "@crowdedkingdoms/crowdyjs",
18
- "version": "18.19.0",
18
+ "version": "18.20.0-dev.1",
19
19
  "source": "node_modules"
20
20
  },
21
21
  "files": {
@@ -112,7 +112,7 @@
112
112
  "manifest.webmanifest": "sha256:9b54d57eea00f2fdd1affe6e7999a4209a515755f12ac1a3299da2d55e178d4c",
113
113
  "preview/bootstrap-CYECZNXt.js": "sha256:9f303a0485c0badfdd19fad0c357ef050c5b9a343744ab5bf0c70015e7d95092",
114
114
  "preview/crowdy-boot-Cklh1qNl.js": "sha256:b21e8eb126e1cd8a52159d2d577e610df6c8af36a436f9ed5028cfabd11a85ff",
115
- "preview/vfs-image.tar.gz": "sha256:e7cccb795f5739e08bdfcd1e34e456a91268de63e3a1131bcddfa540d8e44800",
115
+ "preview/vfs-image.tar.gz": "sha256:32fca538762a73aa62017254efeaeadbede6ae134d9289fa0f0a5b2d5892daca",
116
116
  "preview/worker-C8-_ANaz.js": "sha256:81099e5732dfe0c32bacd5897316b7956c5a0a22da01b4a0c6e21d0a3b9d9aa1"
117
117
  }
118
118
  }
@@ -6752,6 +6752,7 @@ var RealtimeClient = class {
6752
6752
  this.statusListeners = /* @__PURE__ */ new Set();
6753
6753
  this.subscribers = /* @__PURE__ */ new Map();
6754
6754
  this.pending = /* @__PURE__ */ new Map();
6755
+ this.sessionEpochValue = 0;
6755
6756
  this.nextSubscriberId = 1;
6756
6757
  this.subscribedAppId = null;
6757
6758
  this.opening = null;
@@ -6961,16 +6962,69 @@ var RealtimeClient = class {
6961
6962
  });
6962
6963
  }
6963
6964
  waitForSequence(sequenceNumber, timeoutMs = this.waitTimeoutMs) {
6964
- return new Promise((resolve, reject) => {
6965
- const timer = setTimeout(() => {
6966
- this.removePending(sequenceNumber, wait);
6967
- reject(new CrowdyRealtimeError(`Timed out waiting for UDP response sequence ${sequenceNumber}`, { code: "UDP_SEQUENCE_TIMEOUT", retryable: true }));
6968
- }, timeoutMs);
6969
- const wait = { resolve, reject, timer };
6970
- const waits = this.pending.get(sequenceNumber) ?? [];
6971
- waits.push(wait);
6972
- this.pending.set(sequenceNumber, waits);
6965
+ const wait = this.expectSequence(sequenceNumber, timeoutMs);
6966
+ wait.start();
6967
+ return wait.promise;
6968
+ }
6969
+ /**
6970
+ * Register a wait for `sequenceNumber` now and start its clock later.
6971
+ *
6972
+ * An awaited send registers before it sends, so an echo that arrives before
6973
+ * the send's own response is not missed, and calls `start()` once the send has
6974
+ * been accepted: the time the send leg took (a lazy proxy-session open, the
6975
+ * binary relay's handshake) is then not taken from the echo's window.
6976
+ * {@link waitForSequence} is this with the clock started at once.
6977
+ *
6978
+ * @param sequenceNumber - The uint8 sequence the response will carry.
6979
+ * @param timeoutMs - How long to wait once started; defaults to
6980
+ * {@link RealtimeConfig.waitTimeoutMs}.
6981
+ */
6982
+ expectSequence(sequenceNumber, timeoutMs = this.waitTimeoutMs) {
6983
+ let settled = false;
6984
+ let wait;
6985
+ const promise = new Promise((resolve, reject) => {
6986
+ wait = {
6987
+ resolve: (notification) => {
6988
+ settled = true;
6989
+ resolve(notification);
6990
+ },
6991
+ reject: (error) => {
6992
+ settled = true;
6993
+ reject(error);
6994
+ }
6995
+ };
6973
6996
  });
6997
+ promise.catch(() => void 0);
6998
+ const waits = this.pending.get(sequenceNumber) ?? [];
6999
+ waits.push(wait);
7000
+ this.pending.set(sequenceNumber, waits);
7001
+ return {
7002
+ promise,
7003
+ start: () => {
7004
+ if (settled || wait.timer !== void 0)
7005
+ return;
7006
+ wait.timer = setTimeout(() => {
7007
+ this.removePending(sequenceNumber, wait);
7008
+ wait.reject(new CrowdyRealtimeError(`Timed out waiting for UDP response sequence ${sequenceNumber}`, { code: "UDP_SEQUENCE_TIMEOUT", retryable: true }));
7009
+ }, timeoutMs);
7010
+ },
7011
+ cancel: () => {
7012
+ if (wait.timer !== void 0)
7013
+ clearTimeout(wait.timer);
7014
+ settled = true;
7015
+ this.removePending(sequenceNumber, wait);
7016
+ }
7017
+ };
7018
+ }
7019
+ /**
7020
+ * How many times this client's realtime session has been established: every
7021
+ * transition into `connected` (the first connect, each reconnect) and every
7022
+ * move to another instance counts one. An awaited send compares it with the
7023
+ * epoch of its last confirmed response to know it is the first send of a new
7024
+ * session.
7025
+ */
7026
+ sessionEpoch() {
7027
+ return this.sessionEpochValue;
6974
7028
  }
6975
7029
  /**
6976
7030
  * Whether the binary relay transport is currently active and able to carry
@@ -7265,6 +7319,7 @@ var RealtimeClient = class {
7265
7319
  this.wsUrl = wsUrl;
7266
7320
  this.binaryRelayUrl = deriveBinaryRelayUrl(wsUrl);
7267
7321
  this.connectFailures = 0;
7322
+ this.sessionEpochValue += 1;
7268
7323
  this.restartBinaryRelay();
7269
7324
  if (opts.moveNow)
7270
7325
  this.restartGraphqlSubscription();
@@ -7358,6 +7413,7 @@ var RealtimeClient = class {
7358
7413
  this.wsUrl = target.wsUrl;
7359
7414
  this.binaryRelayUrl = deriveBinaryRelayUrl(target.wsUrl);
7360
7415
  this.connectFailures = 0;
7416
+ this.sessionEpochValue += 1;
7361
7417
  this.onEndpointMove?.({ httpUrl: target.httpUrl, wsUrl: target.wsUrl });
7362
7418
  this.restartBinaryRelay();
7363
7419
  }
@@ -7522,6 +7578,8 @@ var RealtimeClient = class {
7522
7578
  if (status === this.statusValue)
7523
7579
  return;
7524
7580
  this.statusValue = status;
7581
+ if (status === "connected")
7582
+ this.sessionEpochValue += 1;
7525
7583
  for (const listener of [...this.statusListeners]) {
7526
7584
  listener(status);
7527
7585
  }
@@ -7644,9 +7702,11 @@ var ActorClient = class {
7644
7702
  * applied echo — your own `ActorUpdateNotification` (the sender is included in
7645
7703
  * the chunk fan-out), correlated by `sequenceNumber`.
7646
7704
  *
7647
- * The **first** spatial message to a brand-new chunk may be dropped
7648
- * server-side while grid permissions load, so if this rejects with a timeout,
7649
- * simply call `join` again (the two-client tests register twice).
7705
+ * The game server holds the first message into a newly entered region until
7706
+ * its permissions load (realtime server v0.42.0 and later). Against an older
7707
+ * server the first awaited send of each session is retried once (see
7708
+ * `UdpAPI.sendActorUpdateAndWait`); if `join` still rejects with a timeout,
7709
+ * call it again.
7650
7710
  *
7651
7711
  * @param chunk - The chunk to enter, `{ x, y, z }` with each axis a signed
7652
7712
  * int64 decimal string. A chunk is a 16x16x16 voxel cube.
@@ -17184,6 +17244,7 @@ var UdpAPI = class {
17184
17244
  this.waitForGameplayTokenRefresh = waitForGameplayTokenRefresh;
17185
17245
  this.sequences = new SequenceAllocator();
17186
17246
  this.inFlightActorUpdates = /* @__PURE__ */ new Set();
17247
+ this.confirmedEpoch = -1;
17187
17248
  }
17188
17249
  /**
17189
17250
  * Resolves when every in-flight {@link sendActorUpdate} has settled.
@@ -17370,8 +17431,22 @@ var UdpAPI = class {
17370
17431
  * @param input - {@link ActorUpdateRequestInput} (see {@link sendActorUpdate}
17371
17432
  * for field units/encoding). Omit `sequenceNumber` to let the SDK allocate
17372
17433
  * one.
17373
- * @param options - `timeoutMs`: how long to wait for the echo, in
17374
- * milliseconds; defaults to the client's realtime `waitTimeoutMs` (5000).
17434
+ * Every `send*AndWait` behaves as follows:
17435
+ * - `timeoutMs` counts from when the send was accepted, not from the call, so
17436
+ * a slow send leg (the first send opening the proxy session, the binary
17437
+ * relay finishing its handshake) does not shorten the wait for the echo.
17438
+ * - The first awaited send of a new session (the first connect, a reconnect,
17439
+ * a move to another instance) is retried once with a fresh sequence number
17440
+ * if it is refused `UNAUTHORIZED` or, for the actor and voxel updates the
17441
+ * server echoes, if its echo times out (`UDP_SEQUENCE_TIMEOUT`), which is
17442
+ * how a game server older than v0.42.0 answers a session's first messages;
17443
+ * the resolved notification then carries the retry's sequence number. A
17444
+ * send whose `sequenceNumber` you chose is not retried, and neither is the
17445
+ * timeout of an audio, text or event send, which is how those succeed.
17446
+ *
17447
+ * @param options - `timeoutMs`: how long to wait for the echo once the send
17448
+ * is accepted, in milliseconds; defaults to the client's realtime
17449
+ * `waitTimeoutMs` (5000).
17375
17450
  * @returns The correlated {@link SpatialNotification} (your own
17376
17451
  * `ActorUpdateNotification` self-echo).
17377
17452
  * @throws {CrowdyGraphQLError} if the underlying send is rejected.
@@ -17379,7 +17454,8 @@ var UdpAPI = class {
17379
17454
  * request timeout.
17380
17455
  * @throws {CrowdyRealtimeError} if no echo arrives within `timeoutMs`
17381
17456
  * (`code === 'UDP_SEQUENCE_TIMEOUT'`, retryable), or the server returns a
17382
- * matching `GenericErrorResponse` (its `code` is the server's `errorCode`).
17457
+ * matching `GenericErrorResponse` (its `code` is the server's `errorCode`),
17458
+ * after the one retry described above when it applies.
17383
17459
  * @example
17384
17460
  * ```ts
17385
17461
  * const unsubscribe = client.udp.subscribe({}, appId); // echoes need an open WS
@@ -17394,11 +17470,7 @@ var UdpAPI = class {
17394
17470
  * ```
17395
17471
  */
17396
17472
  async sendActorUpdateAndWait(input, options = {}) {
17397
- const request = this.withSequence(input);
17398
- const wait = this.subs.waitForSequence(request.sequenceNumber, options.timeoutMs);
17399
- await this.sendActorUpdate(request);
17400
- this.subs.flushSends();
17401
- return wait;
17473
+ return this.sendAndWait(input, options.timeoutMs, true, (request) => this.sendActorUpdate(request));
17402
17474
  }
17403
17475
  /**
17404
17476
  * Send a single voxel (block) update for spatial replication to nearby
@@ -17465,11 +17537,7 @@ var UdpAPI = class {
17465
17537
  * (its `code` is the server's `errorCode`).
17466
17538
  */
17467
17539
  async sendVoxelUpdateAndWait(input, options = {}) {
17468
- const request = this.withSequence(input);
17469
- const wait = this.subs.waitForSequence(request.sequenceNumber, options.timeoutMs);
17470
- await this.sendVoxelUpdate(request);
17471
- this.subs.flushSends();
17472
- return wait;
17540
+ return this.sendAndWait(input, options.timeoutMs, true, (request) => this.sendVoxelUpdate(request));
17473
17541
  }
17474
17542
  /**
17475
17543
  * Send a spatial voice/audio packet, fanned out to nearby actors as a
@@ -17528,11 +17596,7 @@ var UdpAPI = class {
17528
17596
  * `errorCode`, e.g. `UNAUTHORIZED` for a missing voice permission).
17529
17597
  */
17530
17598
  async sendAudioPacketAndWait(input, options = {}) {
17531
- const request = this.withSequence(input);
17532
- const wait = this.subs.waitForSequence(request.sequenceNumber, options.timeoutMs);
17533
- await this.sendAudioPacket(request);
17534
- this.subs.flushSends();
17535
- return wait;
17599
+ return this.sendAndWait(input, options.timeoutMs, false, (request) => this.sendAudioPacket(request));
17536
17600
  }
17537
17601
  /**
17538
17602
  * Send ONE webcam video fragment, fanned out to nearby actors as a
@@ -17643,11 +17707,7 @@ var UdpAPI = class {
17643
17707
  * `errorCode`).
17644
17708
  */
17645
17709
  async sendTextPacketAndWait(input, options = {}) {
17646
- const request = this.withSequence(input);
17647
- const wait = this.subs.waitForSequence(request.sequenceNumber, options.timeoutMs);
17648
- await this.sendTextPacket(request);
17649
- this.subs.flushSends();
17650
- return wait;
17710
+ return this.sendAndWait(input, options.timeoutMs, false, (request) => this.sendTextPacket(request));
17651
17711
  }
17652
17712
  /**
17653
17713
  * Send a custom, app-defined client event for spatial replication to nearby
@@ -17704,11 +17764,7 @@ var UdpAPI = class {
17704
17764
  * `errorCode`).
17705
17765
  */
17706
17766
  async sendClientEventAndWait(input, options = {}) {
17707
- const request = this.withSequence(input);
17708
- const wait = this.subs.waitForSequence(request.sequenceNumber, options.timeoutMs);
17709
- await this.sendClientEvent(request);
17710
- this.subs.flushSends();
17711
- return wait;
17767
+ return this.sendAndWait(input, options.timeoutMs, false, (request) => this.sendClientEvent(request));
17712
17768
  }
17713
17769
  /**
17714
17770
  * Send a direct actor-to-actor message, delivered only to the actor whose
@@ -17923,6 +17979,53 @@ var UdpAPI = class {
17923
17979
  subscribe(handlers, appId) {
17924
17980
  return this.subs.subscribe(handlers, appId);
17925
17981
  }
17982
+ /**
17983
+ * What every `send*AndWait` does.
17984
+ *
17985
+ * The wait is registered before the send, so an echo that beats the send's
17986
+ * own response is not missed, and its clock starts once the send has been
17987
+ * accepted, so a lazy proxy-session open or the relay's handshake on the send
17988
+ * leg is not taken from the echo's window.
17989
+ *
17990
+ * The first awaited send of a new session (the first connect, a reconnect, or
17991
+ * a move to another instance) is retried once, with a fresh sequence number,
17992
+ * when it is refused `UNAUTHORIZED` or, for a send the server echoes, when
17993
+ * its echo times out: a game server from before the one that holds a
17994
+ * session's first messages refuses or drops them while the session's
17995
+ * permissions load. A send the server does not echo (audio, text, a client
17996
+ * event) succeeds by timing out, so its timeout is never retried. Only when
17997
+ * the SDK chose the sequence number, because the retry needs one of its own
17998
+ * and a caller who chose theirs is correlating by it.
17999
+ */
18000
+ async sendAndWait(input, timeoutMs, echoed, send) {
18001
+ const epoch = this.subs.sessionEpoch();
18002
+ const retryable = epoch !== this.confirmedEpoch && input.sequenceNumber == null;
18003
+ try {
18004
+ const echo = await this.sendOnceAndWait(this.withSequence(input), timeoutMs, send).catch((error) => {
18005
+ if (!retryable || !isFirstSendFailure(error, echoed))
18006
+ throw error;
18007
+ return this.sendOnceAndWait(this.withSequence({ ...input, sequenceNumber: void 0 }), timeoutMs, send);
18008
+ });
18009
+ this.confirmedEpoch = epoch;
18010
+ return echo;
18011
+ } catch (error) {
18012
+ if (!echoed && isTimeout(error))
18013
+ this.confirmedEpoch = epoch;
18014
+ throw error;
18015
+ }
18016
+ }
18017
+ async sendOnceAndWait(request, timeoutMs, send) {
18018
+ const wait = this.subs.expectSequence(request.sequenceNumber, timeoutMs);
18019
+ try {
18020
+ await send(request);
18021
+ } catch (error) {
18022
+ wait.cancel();
18023
+ throw error;
18024
+ }
18025
+ this.subs.flushSends();
18026
+ wait.start();
18027
+ return wait.promise;
18028
+ }
17926
18029
  withSequence(input) {
17927
18030
  return {
17928
18031
  ...input,
@@ -17930,6 +18033,12 @@ var UdpAPI = class {
17930
18033
  };
17931
18034
  }
17932
18035
  };
18036
+ function isFirstSendFailure(error, echoed) {
18037
+ return error instanceof CrowdyRealtimeError && (error.code === "UNAUTHORIZED" || echoed && isTimeout(error));
18038
+ }
18039
+ function isTimeout(error) {
18040
+ return error instanceof CrowdyRealtimeError && error.code === "UDP_SEQUENCE_TIMEOUT";
18041
+ }
17933
18042
 
17934
18043
  var STATUS_FIELDS = `
17935
18044
  configured connected accountLogin accountType owner repo branch githubSha repositorySelection installUrl
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@crowdedkingdoms/crowdyjs",
3
- "version": "18.19.0",
3
+ "version": "18.20.0-dev.1",
4
4
  "source": "node_modules"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdedkingdoms/crowdy-dsh",
3
- "version": "0.4.12",
3
+ "version": "0.4.13-dev.1",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -94,11 +94,11 @@
94
94
  "crowdyDsh": {
95
95
  "build": {
96
96
  "package": "@crowdedkingdoms/crowdy-dsh",
97
- "version": "0.4.12",
98
- "builtAt": "2026-10-24T14:23:55.299Z",
97
+ "version": "0.4.13",
98
+ "builtAt": "2026-10-24T21:54:31.702Z",
99
99
  "source": {
100
100
  "repository": "CrowdedKingdoms/crowdy-dsh",
101
- "commit": "da2e4b36025c6f7944bbcf0ed799fabf05c5c044",
101
+ "commit": "a2422f393aebc79753ac80845ed0cee1cb05974e",
102
102
  "dirty": false
103
103
  },
104
104
  "upstream": {
@@ -109,7 +109,7 @@
109
109
  },
110
110
  "crowdyjs": {
111
111
  "name": "@crowdedkingdoms/crowdyjs",
112
- "version": "18.19.0",
112
+ "version": "18.20.0-dev.1",
113
113
  "source": "node_modules"
114
114
  },
115
115
  "files": {
@@ -206,7 +206,7 @@
206
206
  "manifest.webmanifest": "sha256:9b54d57eea00f2fdd1affe6e7999a4209a515755f12ac1a3299da2d55e178d4c",
207
207
  "preview/bootstrap-CYECZNXt.js": "sha256:9f303a0485c0badfdd19fad0c357ef050c5b9a343744ab5bf0c70015e7d95092",
208
208
  "preview/crowdy-boot-Cklh1qNl.js": "sha256:b21e8eb126e1cd8a52159d2d577e610df6c8af36a436f9ed5028cfabd11a85ff",
209
- "preview/vfs-image.tar.gz": "sha256:e7cccb795f5739e08bdfcd1e34e456a91268de63e3a1131bcddfa540d8e44800",
209
+ "preview/vfs-image.tar.gz": "sha256:32fca538762a73aa62017254efeaeadbede6ae134d9289fa0f0a5b2d5892daca",
210
210
  "preview/worker-C8-_ANaz.js": "sha256:81099e5732dfe0c32bacd5897316b7956c5a0a22da01b4a0c6e21d0a3b9d9aa1"
211
211
  }
212
212
  }