@crowdedkingdoms/crowdy-dsh 0.3.0 → 0.3.1-test.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.3.0",
4
- "builtAt": "2026-09-13T18:14:35.893Z",
3
+ "version": "0.3.1",
4
+ "builtAt": "2026-09-13T21:06:31.527Z",
5
5
  "source": {
6
6
  "repository": "CrowdedKingdoms/crowdy-dsh",
7
- "commit": "702cda3c17f7e9c3db831854a38825859ea493ff",
7
+ "commit": "9dbbdc7c9045024c906fae09e65ba4a1708b863d",
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": "17.0.1",
18
+ "version": "17.1.0-test.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:abde28e45be058a6ca93008319306615441e75e4cfee1257ba8cd0781a719021",
115
+ "preview/vfs-image.tar.gz": "sha256:c193c93c3c198e33159217a83f1a5f10dc479eef8e4ed3a44a7170eef34cd098",
116
116
  "preview/worker-C8-_ANaz.js": "sha256:81099e5732dfe0c32bacd5897316b7956c5a0a22da01b4a0c6e21d0a3b9d9aa1"
117
117
  }
118
118
  }
@@ -5814,6 +5814,44 @@ function validateChunkCoordinates(chunk) {
5814
5814
  }
5815
5815
 
5816
5816
  var RELAY_MAX_DATAGRAM_BYTES = 1232;
5817
+ var BUNDLE_HEADER_BYTES = 1;
5818
+ var BUNDLE_LENGTH_PREFIX_BYTES = 2;
5819
+ var RELAY_MAX_BUNDLE_MEMBERS = 32;
5820
+ var RELAY_MAX_BUNDLE_MEMBER_BYTES = RELAY_MAX_DATAGRAM_BYTES - BUNDLE_HEADER_BYTES - BUNDLE_LENGTH_PREFIX_BYTES;
5821
+ function bundleSizeOf(members) {
5822
+ let size = BUNDLE_HEADER_BYTES;
5823
+ for (const m of members)
5824
+ size += BUNDLE_LENGTH_PREFIX_BYTES + m.length;
5825
+ return size;
5826
+ }
5827
+ function packMessageBundle(members) {
5828
+ if (members.length === 0)
5829
+ throw new Error("MESSAGE_BUNDLE needs at least one member");
5830
+ if (members.length > RELAY_MAX_BUNDLE_MEMBERS) {
5831
+ throw new Error(`MESSAGE_BUNDLE holds at most ${RELAY_MAX_BUNDLE_MEMBERS} members`);
5832
+ }
5833
+ for (const m of members) {
5834
+ if (m.length === 0)
5835
+ throw new Error("MESSAGE_BUNDLE member cannot be empty");
5836
+ }
5837
+ if (members.length === 1)
5838
+ return members[0];
5839
+ const size = bundleSizeOf(members);
5840
+ if (size > RELAY_MAX_DATAGRAM_BYTES) {
5841
+ throw new Error(`MESSAGE_BUNDLE of ${size} bytes exceeds ${RELAY_MAX_DATAGRAM_BYTES}`);
5842
+ }
5843
+ const out = new Uint8Array(size);
5844
+ const view = new DataView(out.buffer);
5845
+ out[0] = WireMessageType.MESSAGE_BUNDLE;
5846
+ let off = BUNDLE_HEADER_BYTES;
5847
+ for (const m of members) {
5848
+ view.setUint16(off, m.length, true);
5849
+ off += BUNDLE_LENGTH_PREFIX_BYTES;
5850
+ out.set(m, off);
5851
+ off += m.length;
5852
+ }
5853
+ return out;
5854
+ }
5817
5855
  var WireMessageType = {
5818
5856
  MESSAGE_BUNDLE: 2,
5819
5857
  GENERIC_ERROR_MESSAGE: 3,
@@ -6252,6 +6290,10 @@ function isSameEstate(current, candidate) {
6252
6290
  const site = (h) => h.split(".").slice(-2).join(".");
6253
6291
  return site(a) === site(b) && site(a).includes(".");
6254
6292
  }
6293
+ function documentIsHidden() {
6294
+ const doc = globalThis.document;
6295
+ return doc?.visibilityState === "hidden";
6296
+ }
6255
6297
  function base64UrlEncode(value) {
6256
6298
  return btoa(value).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
6257
6299
  }
@@ -6267,11 +6309,27 @@ var BinaryRelayTransport = class {
6267
6309
  this.everReady = false;
6268
6310
  this.retryTimer = null;
6269
6311
  this.generation = 0;
6312
+ this.pendingMembers = [];
6313
+ this.pendingBytes = 0;
6314
+ this.bundleTimer = null;
6315
+ this.sendStats = {
6316
+ messagesSent: 0,
6317
+ framesSent: 0,
6318
+ bundlesSent: 0,
6319
+ bytesSent: 0,
6320
+ messagesDropped: 0
6321
+ };
6270
6322
  this.url = config.url;
6271
6323
  this.logger = config.logger ?? silentLogger;
6272
6324
  this.retryAttempts = config.retryAttempts ?? 8;
6273
6325
  this.retryInitialDelayMs = config.retryInitialDelayMs ?? 250;
6274
6326
  this.retryMaxDelayMs = config.retryMaxDelayMs ?? 5e3;
6327
+ this.bundleSends = config.bundleSends ?? true;
6328
+ this.bundleWindowMs = Math.max(0, config.bundleWindowMs ?? 1);
6329
+ }
6330
+ /** Snapshot of the uplink counters. */
6331
+ stats() {
6332
+ return { ...this.sendStats };
6275
6333
  }
6276
6334
  /** True when the socket is open and the `ready` handshake completed. */
6277
6335
  isReady() {
@@ -6290,6 +6348,7 @@ var BinaryRelayTransport = class {
6290
6348
  }
6291
6349
  disconnect() {
6292
6350
  this.desired = false;
6351
+ this.flushSends();
6293
6352
  if (this.retryTimer) {
6294
6353
  clearTimeout(this.retryTimer);
6295
6354
  this.retryTimer = null;
@@ -6316,7 +6375,11 @@ var BinaryRelayTransport = class {
6316
6375
  this.appId = appId;
6317
6376
  this.open();
6318
6377
  }
6319
- /** Send one pre-serialized Buddy datagram as a BINARY frame. */
6378
+ /**
6379
+ * Send one pre-serialized, signed Buddy message. With `bundleSends` it joins
6380
+ * the pending MESSAGE_BUNDLE and leaves with it (see {@link flushSends});
6381
+ * otherwise it is one BINARY frame right now.
6382
+ */
6320
6383
  sendFrame(frame) {
6321
6384
  if (!this.isReady() || !this.ws) {
6322
6385
  throw new CrowdyRealtimeError("Binary relay is not connected", {
@@ -6324,7 +6387,63 @@ var BinaryRelayTransport = class {
6324
6387
  retryable: true
6325
6388
  });
6326
6389
  }
6390
+ if (frame.length === 0)
6391
+ return;
6392
+ this.sendStats.messagesSent += 1;
6393
+ if (!this.bundleSends) {
6394
+ this.transmit(frame, false);
6395
+ return;
6396
+ }
6397
+ if (frame.length > RELAY_MAX_BUNDLE_MEMBER_BYTES) {
6398
+ this.flushSends();
6399
+ this.transmit(frame, false);
6400
+ return;
6401
+ }
6402
+ const wouldBe = this.pendingBytes + BUNDLE_LENGTH_PREFIX_BYTES + frame.length;
6403
+ if (this.pendingMembers.length > 0 && (wouldBe > RELAY_MAX_DATAGRAM_BYTES || this.pendingMembers.length >= RELAY_MAX_BUNDLE_MEMBERS)) {
6404
+ this.flushSends();
6405
+ }
6406
+ if (this.pendingMembers.length === 0) {
6407
+ this.pendingBytes = bundleSizeOf([]);
6408
+ this.bundleTimer = setTimeout(() => {
6409
+ this.bundleTimer = null;
6410
+ this.flushSends();
6411
+ }, this.bundleWindowMs);
6412
+ }
6413
+ this.pendingMembers.push(frame);
6414
+ this.pendingBytes += BUNDLE_LENGTH_PREFIX_BYTES + frame.length;
6415
+ if (documentIsHidden())
6416
+ this.flushSends();
6417
+ }
6418
+ /**
6419
+ * Put the pending bundle on the wire now instead of at the end of the
6420
+ * window. Call it at the end of a frame when that frame's sends should not
6421
+ * wait. No-op when nothing is pending.
6422
+ */
6423
+ flushSends() {
6424
+ if (this.bundleTimer) {
6425
+ clearTimeout(this.bundleTimer);
6426
+ this.bundleTimer = null;
6427
+ }
6428
+ const members = this.pendingMembers;
6429
+ if (members.length === 0)
6430
+ return;
6431
+ this.pendingMembers = [];
6432
+ this.pendingBytes = 0;
6433
+ if (!this.isReady() || !this.ws) {
6434
+ this.sendStats.messagesDropped += members.length;
6435
+ return;
6436
+ }
6437
+ this.transmit(packMessageBundle(members), members.length > 1);
6438
+ }
6439
+ transmit(frame, wrapped) {
6440
+ if (!this.ws)
6441
+ return;
6327
6442
  this.ws.send(frame);
6443
+ this.sendStats.framesSent += 1;
6444
+ this.sendStats.bytesSent += frame.length;
6445
+ if (wrapped)
6446
+ this.sendStats.bundlesSent += 1;
6328
6447
  }
6329
6448
  open() {
6330
6449
  const token = this.callbacks.getToken();
@@ -6523,6 +6642,8 @@ var RealtimeClient = class {
6523
6642
  this.lbCookieStore = config.lbCookieStore;
6524
6643
  this.wsUplinkMutations = config.wsUplinkMutations === true;
6525
6644
  this.binaryTransport = config.binaryTransport === true;
6645
+ this.bundleSends = config.bundleSends ?? true;
6646
+ this.bundleWindowMs = config.bundleWindowMs ?? 1;
6526
6647
  this.binaryRelayUrl = config.binaryRelayUrl ?? deriveBinaryRelayUrl(this.wsUrl);
6527
6648
  this.session.onChange((token) => {
6528
6649
  if (!this.desired)
@@ -6736,6 +6857,23 @@ var RealtimeClient = class {
6736
6857
  usingBinaryTransport() {
6737
6858
  return this.binaryTransport && !this.binaryUnavailable;
6738
6859
  }
6860
+ /**
6861
+ * Put the binary relay's pending `MESSAGE_BUNDLE` on the wire now instead of
6862
+ * at the end of {@link RealtimeConfig.bundleWindowMs}. Call it at the end of
6863
+ * a frame when that frame's sends should not wait. No-op on the GraphQL
6864
+ * transport or when nothing is pending.
6865
+ */
6866
+ flushSends() {
6867
+ this.binaryRelay?.flushSends();
6868
+ }
6869
+ /**
6870
+ * Uplink counters of the binary relay (messages, frames, bundles, bytes,
6871
+ * drops), or `null` when the relay has not been created. These are a local
6872
+ * diagnostic, not a bill.
6873
+ */
6874
+ binaryRelayStats() {
6875
+ return this.binaryRelay?.stats() ?? null;
6876
+ }
6739
6877
  /**
6740
6878
  * Serialize (with the session signing context) and send one datagram over
6741
6879
  * the binary relay. Throws `BINARY_RELAY_UNAVAILABLE` when the relay is not
@@ -6938,7 +7076,9 @@ var RealtimeClient = class {
6938
7076
  retryAttempts: this.retryAttempts,
6939
7077
  retryInitialDelayMs: this.retryInitialDelayMs,
6940
7078
  retryMaxDelayMs: this.retryMaxDelayMs,
6941
- logger: this.logger
7079
+ logger: this.logger,
7080
+ bundleSends: this.bundleSends,
7081
+ bundleWindowMs: this.bundleWindowMs
6942
7082
  }, {
6943
7083
  getToken: () => this.session.getToken(),
6944
7084
  onNotification: (notification) => this.dispatch(notification),
@@ -15786,6 +15926,15 @@ var UdpAPI = class {
15786
15926
  * @returns `true` once the session has been closed.
15787
15927
  * @throws {CrowdyGraphQLError} on auth failures.
15788
15928
  */
15929
+ /**
15930
+ * Binary relay only: put the pending `MESSAGE_BUNDLE` on the wire now
15931
+ * instead of at the end of `realtime.bundleWindowMs`. Call it at the end of a
15932
+ * frame whose sends should not wait. No-op on the GraphQL transport, when
15933
+ * `realtime.bundleSends` is false, or when nothing is pending.
15934
+ */
15935
+ flushSends() {
15936
+ this.subs.flushSends();
15937
+ }
15789
15938
  async disconnect() {
15790
15939
  if (this.subs.usingBinaryTransport()) {
15791
15940
  this.subs.disconnect();
@@ -15905,6 +16054,7 @@ var UdpAPI = class {
15905
16054
  const request = this.withSequence(input);
15906
16055
  const wait = this.subs.waitForSequence(request.sequenceNumber, options.timeoutMs);
15907
16056
  await this.sendActorUpdate(request);
16057
+ this.subs.flushSends();
15908
16058
  return wait;
15909
16059
  }
15910
16060
  /**
@@ -15972,6 +16122,7 @@ var UdpAPI = class {
15972
16122
  const request = this.withSequence(input);
15973
16123
  const wait = this.subs.waitForSequence(request.sequenceNumber, options.timeoutMs);
15974
16124
  await this.sendVoxelUpdate(request);
16125
+ this.subs.flushSends();
15975
16126
  return wait;
15976
16127
  }
15977
16128
  /**
@@ -16030,6 +16181,7 @@ var UdpAPI = class {
16030
16181
  const request = this.withSequence(input);
16031
16182
  const wait = this.subs.waitForSequence(request.sequenceNumber, options.timeoutMs);
16032
16183
  await this.sendAudioPacket(request);
16184
+ this.subs.flushSends();
16033
16185
  return wait;
16034
16186
  }
16035
16187
  /**
@@ -16141,6 +16293,7 @@ var UdpAPI = class {
16141
16293
  const request = this.withSequence(input);
16142
16294
  const wait = this.subs.waitForSequence(request.sequenceNumber, options.timeoutMs);
16143
16295
  await this.sendTextPacket(request);
16296
+ this.subs.flushSends();
16144
16297
  return wait;
16145
16298
  }
16146
16299
  /**
@@ -16201,6 +16354,7 @@ var UdpAPI = class {
16201
16354
  const request = this.withSequence(input);
16202
16355
  const wait = this.subs.waitForSequence(request.sequenceNumber, options.timeoutMs);
16203
16356
  await this.sendClientEvent(request);
16357
+ this.subs.flushSends();
16204
16358
  return wait;
16205
16359
  }
16206
16360
  /**
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@crowdedkingdoms/crowdyjs",
3
- "version": "17.0.1",
3
+ "version": "17.1.0-test.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.3.0",
3
+ "version": "0.3.1-test.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.3.0",
98
- "builtAt": "2026-09-13T18:14:35.893Z",
97
+ "version": "0.3.1",
98
+ "builtAt": "2026-09-13T21:06:31.527Z",
99
99
  "source": {
100
100
  "repository": "CrowdedKingdoms/crowdy-dsh",
101
- "commit": "702cda3c17f7e9c3db831854a38825859ea493ff",
101
+ "commit": "9dbbdc7c9045024c906fae09e65ba4a1708b863d",
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": "17.0.1",
112
+ "version": "17.1.0-test.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:abde28e45be058a6ca93008319306615441e75e4cfee1257ba8cd0781a719021",
209
+ "preview/vfs-image.tar.gz": "sha256:c193c93c3c198e33159217a83f1a5f10dc479eef8e4ed3a44a7170eef34cd098",
210
210
  "preview/worker-C8-_ANaz.js": "sha256:81099e5732dfe0c32bacd5897316b7956c5a0a22da01b4a0c6e21d0a3b9d9aa1"
211
211
  }
212
212
  }