@bountyboard/arcade-sdk 1.0.1 → 1.1.0

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/AGENTS.md CHANGED
@@ -72,7 +72,9 @@ the safe/default experience the alphabetically-first name. Don't re-randomize cl
72
72
  assert that `show()` is called directly by your UI handler and only final status `'viewed'`
73
73
  grants the reward.
74
74
  - Multiplayer: run the room server locally (`wrangler dev` with `DEV_ALLOW_UNSIGNED=1`) and pass
75
- `{ roomUrl, ticket }` overrides to `joinRoom` for development.
75
+ `{ roomUrl, ticket }` overrides to `joinRoom` for development. Exercise a disconnect/reconnect,
76
+ assert `trySend()` returns false while offline, and verify the authoritative module validates the
77
+ optional `joinData` rather than treating avatar/loadout/mode selections as trusted state.
76
78
 
77
79
  ## Design intent (read docs/game-design-playbook.md before building a NEW game)
78
80
 
package/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # @bountyboard/arcade-sdk
2
+
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 10a4997: Support production real-time game modules with bounded join data, observable reconnect state, and explicit `trySend` backpressure.
8
+
9
+ ### Patch Changes
10
+
11
+ - f701c2c: Require Bounty Board-approved, host-session-bound tickets for multiplayer rooms and harden the authoritative room server's edge authorization.
12
+ - 732b929: Keep the Bounty Board host fullscreen overlay from covering provider iframe ad
13
+ controls on touch devices.
14
+ - 72daa42: Ship the package changelog and add reproducible release validation for npm,
15
+ browser-script, ESM, CommonJS, TypeScript, global, and multiplayer consumers.
16
+
17
+ ## 1.0.1
18
+
19
+ ### Patch Changes
20
+
21
+ - Added a two-stage rewarded-ad preparation flow that preserves the browser's
22
+ direct user activation requirement, with expanded integration guidance and
23
+ regression coverage.
24
+
25
+ ## 1.0.0
26
+
27
+ ### Major Changes
28
+
29
+ - Initial public release with leaderboard, cloud-save, rewarded-ad,
30
+ experimentation, player, sitelock, and multiplayer APIs.
package/README.md CHANGED
@@ -95,21 +95,31 @@ not break, but new integrations should use the prepared flow because an asynchro
95
95
  ```ts
96
96
  import { joinRoom } from '@bountyboard/arcade-sdk/multiplayer';
97
97
 
98
- const room = await joinRoom({ create: true }); // or { code: 'ABCD' }
98
+ const room = await joinRoom({
99
+ create: true, // or code: 'ABCD'
100
+ joinData: { avatar: 'golem', mode: 'ffa' }, // untrusted, validated by your module
101
+ });
99
102
  room.on('snapshot', ({ state }) => render(state)); // server-authoritative state
100
103
  room.on('end', ({ results }) => showResults(results));
101
- room.send({ move: dir }); // inputs only — the server simulates
104
+ room.on('connection', status => showReconnecting(!status.connected));
105
+ if (!room.trySend({ move: dir })) showReconnecting(true); // inputs only
102
106
  ```
103
107
 
104
108
  Rooms run on Bounty Board's authoritative room servers: clients send inputs, the server
105
109
  validates and simulates, and each player receives only the state they're allowed to see
106
110
  (your hiders stay hidden). Auth is a short-lived signed ticket the SDK obtains from the
107
111
  host automatically; off Bounty Board `joinRoom` rejects with `code: 'unsupported'`.
112
+ `joinData` is limited to a 1 KiB JSON object, is never trusted or broadcast by the
113
+ platform, and must not contain secrets. The room runtime supports separate simulation
114
+ and snapshot rates (for example 60 Hz simulation / 30 Hz snapshots), a 15-second seat
115
+ grace for automatic reconnects, and bounded server frames.
108
116
 
109
117
  Multiplayer is currently a curated integration: Bounty Board must add and deploy a
110
118
  server-side game module for your slug before `joinRoom()` can accept players. The
111
119
  initial reference module is `hide-and-seek`; arbitrary games do not become
112
- authoritative multiplayer games merely by importing the client SDK.
120
+ authoritative multiplayer games merely by importing the client SDK. A production
121
+ module must validate join data and every input, clear held controls on disconnect,
122
+ serialize only per-viewer-safe state, and return final authoritative standings.
113
123
 
114
124
  ## Docs
115
125
 
@@ -220,6 +220,13 @@ interface BBArcadeMpJoinOptions {
220
220
  code?: string;
221
221
  /** Create a new room (a share code is generated for you). */
222
222
  create?: boolean;
223
+ /**
224
+ * Game-defined, JSON-serializable data supplied to the authoritative module
225
+ * before it admits this player (avatar/loadout/mode selections, for example).
226
+ * The room server treats this as untrusted input and limits it to 1 KiB.
227
+ * Never put credentials or other secrets here.
228
+ */
229
+ joinData?: Readonly<Record<string, unknown>>;
223
230
  /**
224
231
  * Local development only: connect straight to a room server (e.g. wrangler
225
232
  * dev with DEV_ALLOW_UNSIGNED=1) instead of asking the Bounty Board host for
@@ -240,6 +247,8 @@ interface BBArcadeMpRoomEvents {
240
247
  /** The match finished; results were reported by the server, not by clients. */
241
248
  end: { results: BBArcadeMpResult[] };
242
249
  error: { code: string };
250
+ /** Connection state, including successful automatic reconnection. */
251
+ connection: { connected: boolean; reconnecting: boolean };
243
252
  /** Connection dropped. reconnecting: true means the SDK is retrying. */
244
253
  close: { reconnecting: boolean };
245
254
  }
@@ -254,8 +263,12 @@ interface BBArcadeMpRoom {
254
263
  players: BBArcadeMpPlayer[];
255
264
  /** Latest authoritative state snapshot (as filtered for you). */
256
265
  state: unknown;
266
+ /** Whether the room currently has an accepted WebSocket connection. */
267
+ connected: boolean;
257
268
  /** Send an INPUT to the server (never authoritative state — the server simulates). */
258
269
  send(input: unknown): void;
270
+ /** Like send(), but reports false when disconnected or the socket raced closed. */
271
+ trySend(input: unknown): boolean;
259
272
  /** Subscribe to a room event; returns an unsubscribe function. */
260
273
  on<K extends keyof BBArcadeMpRoomEvents>(
261
274
  event: K,
package/dist/global.js CHANGED
@@ -884,6 +884,7 @@
884
884
  // src/multiplayer.ts
885
885
  var WELCOME_TIMEOUT_MS = 1e4;
886
886
  var MAX_RECONNECT_ATTEMPTS = 3;
887
+ var MAX_JOIN_DATA_BYTES = 1024;
887
888
  var CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
888
889
  var CODE_LENGTH = 4;
889
890
  function sdkError3(code) {
@@ -905,15 +906,42 @@
905
906
  function generateCode() {
906
907
  return randomChars(CODE_LENGTH);
907
908
  }
908
- var guestKey = null;
909
- function getGuestKey() {
910
- if (!guestKey) guestKey = randomChars(16);
911
- return guestKey;
909
+ function utf8ByteLength(value) {
910
+ let bytes = 0;
911
+ for (let i = 0; i < value.length; i++) {
912
+ const code = value.charCodeAt(i);
913
+ if (code < 128) bytes += 1;
914
+ else if (code < 2048) bytes += 2;
915
+ else if (code >= 55296 && code <= 56319 && i + 1 < value.length) {
916
+ const next = value.charCodeAt(i + 1);
917
+ if (next >= 56320 && next <= 57343) {
918
+ bytes += 4;
919
+ i += 1;
920
+ } else {
921
+ bytes += 3;
922
+ }
923
+ } else bytes += 3;
924
+ }
925
+ return bytes;
926
+ }
927
+ function serializeJoinData(data) {
928
+ if (data === void 0) return null;
929
+ if (!data || typeof data !== "object" || Array.isArray(data)) throw sdkError3("rejected");
930
+ let serialized;
931
+ try {
932
+ serialized = JSON.stringify(data);
933
+ } catch (e) {
934
+ throw sdkError3("rejected");
935
+ }
936
+ if (!serialized || utf8ByteLength(serialized) > MAX_JOIN_DATA_BYTES) {
937
+ throw sdkError3("rejected");
938
+ }
939
+ return serialized;
912
940
  }
913
941
  function requestTicket(roomId) {
914
942
  const request = BBArcade._request;
915
943
  if (typeof request !== "function") return Promise.reject(sdkError3("unsupported"));
916
- return request("mp_ticket", { roomId, guestKey: getGuestKey() }).then((grant) => {
944
+ return request("mp_ticket", { roomId }).then((grant) => {
917
945
  const g = grant;
918
946
  if (!g || typeof g.ticket !== "string" || typeof g.roomUrl !== "string") {
919
947
  throw sdkError3("error");
@@ -922,10 +950,11 @@
922
950
  });
923
951
  }
924
952
  var RoomConnection = class {
925
- constructor(code, devGrant) {
953
+ constructor(code, devGrant, joinData) {
926
954
  this.playerId = "";
927
955
  this.players = [];
928
956
  this.state = null;
957
+ this.connected = false;
929
958
  this.ws = null;
930
959
  // Keyed by event name; values are erased to (unknown) => void internally and
931
960
  // retyped at the on()/emit() boundary, which keeps the public API strictly
@@ -940,12 +969,13 @@
940
969
  this.reconnectAttempts = 0;
941
970
  this.code = code;
942
971
  this.devGrant = devGrant;
972
+ this.joinData = joinData;
943
973
  }
944
974
  /** Connect and resolve on the server's welcome (post-ticket-verification). */
945
975
  connect(grant) {
946
976
  return new Promise((resolve, reject) => {
947
977
  let settled = false;
948
- const url = grant.roomUrl + (grant.roomUrl.indexOf("?") === -1 ? "?" : "&") + "ticket=" + encodeURIComponent(grant.ticket);
978
+ const url = grant.roomUrl + (grant.roomUrl.indexOf("?") === -1 ? "?" : "&") + "ticket=" + encodeURIComponent(grant.ticket) + (this.joinData === null ? "" : "&join=" + encodeURIComponent(this.joinData));
949
979
  let ws;
950
980
  try {
951
981
  ws = new WebSocket(url);
@@ -973,15 +1003,18 @@
973
1003
  }
974
1004
  if (!msg || typeof msg.t !== "string") return;
975
1005
  if (msg.t === "welcome") {
1006
+ const reconnected = this.reconnectAttempts > 0;
976
1007
  this.playerId = String((_a = msg.playerId) != null ? _a : "");
977
1008
  this.players = Array.isArray(msg.players) ? msg.players : [];
978
1009
  this.state = (_b = msg.state) != null ? _b : null;
1010
+ this.connected = true;
979
1011
  this.reconnectAttempts = 0;
980
1012
  if (!settled) {
981
1013
  settled = true;
982
1014
  clearTimeout(welcomeTimer);
983
1015
  resolve(this);
984
1016
  }
1017
+ if (reconnected) this.emit("connection", { connected: true, reconnecting: false });
985
1018
  return;
986
1019
  }
987
1020
  if (msg.t === "snapshot") {
@@ -1031,6 +1064,7 @@
1031
1064
  }
1032
1065
  };
1033
1066
  ws.onclose = () => {
1067
+ this.connected = false;
1034
1068
  if (!settled) {
1035
1069
  settled = true;
1036
1070
  clearTimeout(welcomeTimer);
@@ -1038,6 +1072,7 @@
1038
1072
  return;
1039
1073
  }
1040
1074
  if (this.intentionalClose || this.ended || this.terminal) {
1075
+ this.emit("connection", { connected: false, reconnecting: false });
1041
1076
  this.emit("close", { reconnecting: false });
1042
1077
  return;
1043
1078
  }
@@ -1055,6 +1090,7 @@
1055
1090
  return;
1056
1091
  }
1057
1092
  this.reconnectAttempts += 1;
1093
+ this.emit("connection", { connected: false, reconnecting: true });
1058
1094
  this.emit("close", { reconnecting: true });
1059
1095
  await new Promise((r) => setTimeout(r, 400 * this.reconnectAttempts));
1060
1096
  if (this.intentionalClose || this.ended || this.terminal) return;
@@ -1068,11 +1104,16 @@
1068
1104
  }
1069
1105
  }
1070
1106
  send(input) {
1071
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
1107
+ this.trySend(input);
1108
+ }
1109
+ trySend(input) {
1110
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return false;
1072
1111
  this.seq += 1;
1073
1112
  try {
1074
1113
  this.ws.send(JSON.stringify({ t: "input", seq: this.seq, data: input }));
1114
+ return true;
1075
1115
  } catch (err) {
1116
+ return false;
1076
1117
  }
1077
1118
  }
1078
1119
  on(event, handler) {
@@ -1088,6 +1129,7 @@
1088
1129
  leave() {
1089
1130
  var _a;
1090
1131
  this.intentionalClose = true;
1132
+ this.connected = false;
1091
1133
  try {
1092
1134
  (_a = this.ws) == null ? void 0 : _a.close(1e3);
1093
1135
  } catch (err) {
@@ -1113,11 +1155,17 @@
1113
1155
  return Promise.reject(sdkError3("unsupported"));
1114
1156
  }
1115
1157
  const code = opts.create || !opts.code ? generateCode() : opts.code.toUpperCase();
1158
+ let joinData;
1159
+ try {
1160
+ joinData = serializeJoinData(opts.joinData);
1161
+ } catch (err) {
1162
+ return Promise.reject(err);
1163
+ }
1116
1164
  if (opts.roomUrl && opts.ticket) {
1117
1165
  const devGrant = { ticket: opts.ticket, roomUrl: opts.roomUrl, expiresAt: 0 };
1118
- return new RoomConnection(code, devGrant).connect(devGrant);
1166
+ return new RoomConnection(code, devGrant, joinData).connect(devGrant);
1119
1167
  }
1120
- return requestTicket(code).then((grant) => new RoomConnection(code, null).connect(grant));
1168
+ return requestTicket(code).then((grant) => new RoomConnection(code, null, joinData).connect(grant));
1121
1169
  }
1122
1170
  var multiplayer = { joinRoom };
1123
1171
 
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { B as BBArcadeSDK } from './types-BKChtV3H.cjs';
2
- export { a as BBArcadeConfig, b as BBArcadeError, c as BBArcadeErrorCode, d as BBArcadeLockToHostOptions, e as BBArcadePlayer, f as BBArcadePrepareRewardedAdOptions, g as BBArcadePreparedRewardedAd, h as BBArcadeRewardedAdOptions, i as BBArcadeRewardedAdPreparation, j as BBArcadeRewardedAdPrepareFailure, k as BBArcadeRewardedAdResult, l as BBArcadeRewardedAdStatus, m as BBArcadeRewardedAdsConfig, n as BBArcadeRewardedBreakOptions } from './types-BKChtV3H.cjs';
1
+ import { B as BBArcadeSDK } from './types-Cq8p7ZkC.cjs';
2
+ export { a as BBArcadeConfig, b as BBArcadeError, c as BBArcadeErrorCode, d as BBArcadeLockToHostOptions, e as BBArcadePlayer, f as BBArcadePrepareRewardedAdOptions, g as BBArcadePreparedRewardedAd, h as BBArcadeRewardedAdOptions, i as BBArcadeRewardedAdPreparation, j as BBArcadeRewardedAdPrepareFailure, k as BBArcadeRewardedAdResult, l as BBArcadeRewardedAdStatus, m as BBArcadeRewardedAdsConfig, n as BBArcadeRewardedBreakOptions } from './types-Cq8p7ZkC.cjs';
3
3
 
4
4
  /**
5
5
  * Wire-protocol constants for the bb-arcade postMessage protocol. The protocol
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { B as BBArcadeSDK } from './types-BKChtV3H.js';
2
- export { a as BBArcadeConfig, b as BBArcadeError, c as BBArcadeErrorCode, d as BBArcadeLockToHostOptions, e as BBArcadePlayer, f as BBArcadePrepareRewardedAdOptions, g as BBArcadePreparedRewardedAd, h as BBArcadeRewardedAdOptions, i as BBArcadeRewardedAdPreparation, j as BBArcadeRewardedAdPrepareFailure, k as BBArcadeRewardedAdResult, l as BBArcadeRewardedAdStatus, m as BBArcadeRewardedAdsConfig, n as BBArcadeRewardedBreakOptions } from './types-BKChtV3H.js';
1
+ import { B as BBArcadeSDK } from './types-Cq8p7ZkC.js';
2
+ export { a as BBArcadeConfig, b as BBArcadeError, c as BBArcadeErrorCode, d as BBArcadeLockToHostOptions, e as BBArcadePlayer, f as BBArcadePrepareRewardedAdOptions, g as BBArcadePreparedRewardedAd, h as BBArcadeRewardedAdOptions, i as BBArcadeRewardedAdPreparation, j as BBArcadeRewardedAdPrepareFailure, k as BBArcadeRewardedAdResult, l as BBArcadeRewardedAdStatus, m as BBArcadeRewardedAdsConfig, n as BBArcadeRewardedBreakOptions } from './types-Cq8p7ZkC.js';
3
3
 
4
4
  /**
5
5
  * Wire-protocol constants for the bb-arcade postMessage protocol. The protocol
@@ -909,6 +909,7 @@ var BBArcade = typeof window === "undefined" ? createInertBBArcade() : createBBA
909
909
  // src/multiplayer.ts
910
910
  var WELCOME_TIMEOUT_MS = 1e4;
911
911
  var MAX_RECONNECT_ATTEMPTS = 3;
912
+ var MAX_JOIN_DATA_BYTES = 1024;
912
913
  var CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
913
914
  var CODE_LENGTH = 4;
914
915
  function sdkError3(code) {
@@ -930,15 +931,42 @@ function randomChars(length) {
930
931
  function generateCode() {
931
932
  return randomChars(CODE_LENGTH);
932
933
  }
933
- var guestKey = null;
934
- function getGuestKey() {
935
- if (!guestKey) guestKey = randomChars(16);
936
- return guestKey;
934
+ function utf8ByteLength(value) {
935
+ let bytes = 0;
936
+ for (let i = 0; i < value.length; i++) {
937
+ const code = value.charCodeAt(i);
938
+ if (code < 128) bytes += 1;
939
+ else if (code < 2048) bytes += 2;
940
+ else if (code >= 55296 && code <= 56319 && i + 1 < value.length) {
941
+ const next = value.charCodeAt(i + 1);
942
+ if (next >= 56320 && next <= 57343) {
943
+ bytes += 4;
944
+ i += 1;
945
+ } else {
946
+ bytes += 3;
947
+ }
948
+ } else bytes += 3;
949
+ }
950
+ return bytes;
951
+ }
952
+ function serializeJoinData(data) {
953
+ if (data === void 0) return null;
954
+ if (!data || typeof data !== "object" || Array.isArray(data)) throw sdkError3("rejected");
955
+ let serialized;
956
+ try {
957
+ serialized = JSON.stringify(data);
958
+ } catch (e) {
959
+ throw sdkError3("rejected");
960
+ }
961
+ if (!serialized || utf8ByteLength(serialized) > MAX_JOIN_DATA_BYTES) {
962
+ throw sdkError3("rejected");
963
+ }
964
+ return serialized;
937
965
  }
938
966
  function requestTicket(roomId) {
939
967
  const request = BBArcade._request;
940
968
  if (typeof request !== "function") return Promise.reject(sdkError3("unsupported"));
941
- return request("mp_ticket", { roomId, guestKey: getGuestKey() }).then((grant) => {
969
+ return request("mp_ticket", { roomId }).then((grant) => {
942
970
  const g = grant;
943
971
  if (!g || typeof g.ticket !== "string" || typeof g.roomUrl !== "string") {
944
972
  throw sdkError3("error");
@@ -947,10 +975,11 @@ function requestTicket(roomId) {
947
975
  });
948
976
  }
949
977
  var RoomConnection = class {
950
- constructor(code, devGrant) {
978
+ constructor(code, devGrant, joinData) {
951
979
  this.playerId = "";
952
980
  this.players = [];
953
981
  this.state = null;
982
+ this.connected = false;
954
983
  this.ws = null;
955
984
  // Keyed by event name; values are erased to (unknown) => void internally and
956
985
  // retyped at the on()/emit() boundary, which keeps the public API strictly
@@ -965,12 +994,13 @@ var RoomConnection = class {
965
994
  this.reconnectAttempts = 0;
966
995
  this.code = code;
967
996
  this.devGrant = devGrant;
997
+ this.joinData = joinData;
968
998
  }
969
999
  /** Connect and resolve on the server's welcome (post-ticket-verification). */
970
1000
  connect(grant) {
971
1001
  return new Promise((resolve, reject) => {
972
1002
  let settled = false;
973
- const url = grant.roomUrl + (grant.roomUrl.indexOf("?") === -1 ? "?" : "&") + "ticket=" + encodeURIComponent(grant.ticket);
1003
+ const url = grant.roomUrl + (grant.roomUrl.indexOf("?") === -1 ? "?" : "&") + "ticket=" + encodeURIComponent(grant.ticket) + (this.joinData === null ? "" : "&join=" + encodeURIComponent(this.joinData));
974
1004
  let ws;
975
1005
  try {
976
1006
  ws = new WebSocket(url);
@@ -998,15 +1028,18 @@ var RoomConnection = class {
998
1028
  }
999
1029
  if (!msg || typeof msg.t !== "string") return;
1000
1030
  if (msg.t === "welcome") {
1031
+ const reconnected = this.reconnectAttempts > 0;
1001
1032
  this.playerId = String((_a = msg.playerId) != null ? _a : "");
1002
1033
  this.players = Array.isArray(msg.players) ? msg.players : [];
1003
1034
  this.state = (_b = msg.state) != null ? _b : null;
1035
+ this.connected = true;
1004
1036
  this.reconnectAttempts = 0;
1005
1037
  if (!settled) {
1006
1038
  settled = true;
1007
1039
  clearTimeout(welcomeTimer);
1008
1040
  resolve(this);
1009
1041
  }
1042
+ if (reconnected) this.emit("connection", { connected: true, reconnecting: false });
1010
1043
  return;
1011
1044
  }
1012
1045
  if (msg.t === "snapshot") {
@@ -1056,6 +1089,7 @@ var RoomConnection = class {
1056
1089
  }
1057
1090
  };
1058
1091
  ws.onclose = () => {
1092
+ this.connected = false;
1059
1093
  if (!settled) {
1060
1094
  settled = true;
1061
1095
  clearTimeout(welcomeTimer);
@@ -1063,6 +1097,7 @@ var RoomConnection = class {
1063
1097
  return;
1064
1098
  }
1065
1099
  if (this.intentionalClose || this.ended || this.terminal) {
1100
+ this.emit("connection", { connected: false, reconnecting: false });
1066
1101
  this.emit("close", { reconnecting: false });
1067
1102
  return;
1068
1103
  }
@@ -1080,6 +1115,7 @@ var RoomConnection = class {
1080
1115
  return;
1081
1116
  }
1082
1117
  this.reconnectAttempts += 1;
1118
+ this.emit("connection", { connected: false, reconnecting: true });
1083
1119
  this.emit("close", { reconnecting: true });
1084
1120
  await new Promise((r) => setTimeout(r, 400 * this.reconnectAttempts));
1085
1121
  if (this.intentionalClose || this.ended || this.terminal) return;
@@ -1093,11 +1129,16 @@ var RoomConnection = class {
1093
1129
  }
1094
1130
  }
1095
1131
  send(input) {
1096
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
1132
+ this.trySend(input);
1133
+ }
1134
+ trySend(input) {
1135
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return false;
1097
1136
  this.seq += 1;
1098
1137
  try {
1099
1138
  this.ws.send(JSON.stringify({ t: "input", seq: this.seq, data: input }));
1139
+ return true;
1100
1140
  } catch (err) {
1141
+ return false;
1101
1142
  }
1102
1143
  }
1103
1144
  on(event, handler) {
@@ -1113,6 +1154,7 @@ var RoomConnection = class {
1113
1154
  leave() {
1114
1155
  var _a;
1115
1156
  this.intentionalClose = true;
1157
+ this.connected = false;
1116
1158
  try {
1117
1159
  (_a = this.ws) == null ? void 0 : _a.close(1e3);
1118
1160
  } catch (err) {
@@ -1138,11 +1180,17 @@ function joinRoom(options) {
1138
1180
  return Promise.reject(sdkError3("unsupported"));
1139
1181
  }
1140
1182
  const code = opts.create || !opts.code ? generateCode() : opts.code.toUpperCase();
1183
+ let joinData;
1184
+ try {
1185
+ joinData = serializeJoinData(opts.joinData);
1186
+ } catch (err) {
1187
+ return Promise.reject(err);
1188
+ }
1141
1189
  if (opts.roomUrl && opts.ticket) {
1142
1190
  const devGrant = { ticket: opts.ticket, roomUrl: opts.roomUrl, expiresAt: 0 };
1143
- return new RoomConnection(code, devGrant).connect(devGrant);
1191
+ return new RoomConnection(code, devGrant, joinData).connect(devGrant);
1144
1192
  }
1145
- return requestTicket(code).then((grant) => new RoomConnection(code, null).connect(grant));
1193
+ return requestTicket(code).then((grant) => new RoomConnection(code, null, joinData).connect(grant));
1146
1194
  }
1147
1195
  var multiplayer = { joinRoom };
1148
1196
  // Annotate the CommonJS export names for ESM import in node: