@bountyboard/arcade-sdk 1.1.0 → 1.2.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.
@@ -37,6 +37,8 @@ var AD_PLACEMENT_SRC = "https://pagead2.googlesyndication.com/pagead/js/adsbygoo
37
37
  var AD_PLACEMENT_SCRIPT_ID = "bb-arcade-h5-ads";
38
38
  var DEFAULT_AD_LOAD_TIMEOUT_MS = 6e3;
39
39
  var SAVE_TIMEOUT_MS = 15e3;
40
+ var LOCK_TIMEOUT_MS = 5e3;
41
+ var MAX_SAVE_BYTES = 1024 * 1024;
40
42
  var INIT_CONFIG_GRACE_MS = 1500;
41
43
  var DEFAULT_ALLOW = [
42
44
  "bountyboard.gg",
@@ -61,6 +63,14 @@ function cleanText(value, fallback, max) {
61
63
  if (typeof value !== "string") return fallback || "";
62
64
  return value.replace(/[^\w:.-]+/g, "_").slice(0, max || 80) || fallback || "";
63
65
  }
66
+ var cachedTextEncoder = null;
67
+ function utf8ByteLength(value) {
68
+ if (typeof TextEncoder !== "undefined") {
69
+ cachedTextEncoder = cachedTextEncoder || new TextEncoder();
70
+ return cachedTextEncoder.encode(value).length;
71
+ }
72
+ return new Blob([value]).size;
73
+ }
64
74
  function toBoolean(value) {
65
75
  return value === true || value === "true" || value === "1";
66
76
  }
@@ -180,13 +190,14 @@ function createBBArcade() {
180
190
  };
181
191
  let hostConfigReceived = false;
182
192
  let hostPlayer = null;
193
+ const playerChangeHandlers = [];
183
194
  let blocked = false;
184
195
  const pending = {};
185
196
  let seq = 0;
186
197
  function post(type, score, extra) {
187
198
  if (blocked) return;
188
199
  const message = { source: SDK_MESSAGE_SOURCE, v: VERSION, type };
189
- if (typeof score === "number" && isFinite(score)) message.score = Math.floor(score);
200
+ if (typeof score === "number" && isFinite(score)) message.score = Math.trunc(score);
190
201
  if (extra && typeof extra === "object") {
191
202
  const fields = extra;
192
203
  for (const key in fields) {
@@ -207,7 +218,7 @@ function createBBArcade() {
207
218
  return true;
208
219
  }
209
220
  }
210
- function request(type, payload) {
221
+ function request(type, payload, timeoutMs = SAVE_TIMEOUT_MS) {
211
222
  return new Promise(function(resolve, reject) {
212
223
  if (blocked) return reject(sdkError("error"));
213
224
  if (!isEmbedded()) return reject(sdkError("unsupported"));
@@ -217,7 +228,7 @@ function createBBArcade() {
217
228
  delete pending[requestId];
218
229
  reject(sdkError("error"));
219
230
  }
220
- }, SAVE_TIMEOUT_MS);
231
+ }, timeoutMs);
221
232
  pending[requestId] = { resolve, reject, timer };
222
233
  try {
223
234
  window.parent.postMessage(
@@ -309,7 +320,7 @@ function createBBArcade() {
309
320
  if (!hostnameAllowed(origin, allow)) blockHost(options);
310
321
  return;
311
322
  }
312
- request("host").then(
323
+ request("host", void 0, LOCK_TIMEOUT_MS).then(
313
324
  function(res) {
314
325
  const r = res;
315
326
  if (!hostnameAllowed(r && r.origin, allow)) blockHost(options);
@@ -333,7 +344,7 @@ function createBBArcade() {
333
344
  lockToHostBestEffort(opts, allow);
334
345
  return;
335
346
  }
336
- request("host_attest").then(
347
+ request("host_attest", void 0, LOCK_TIMEOUT_MS).then(
337
348
  function(res) {
338
349
  const r = res;
339
350
  if (!r || !r.attestation) {
@@ -395,7 +406,15 @@ function createBBArcade() {
395
406
  if (msg.source !== HOST_MESSAGE_SOURCE || msg.type !== "config") return;
396
407
  hostConfigReceived = true;
397
408
  if (msg.player !== void 0) {
398
- hostPlayer = sanitizePlayer(msg.player);
409
+ const nextPlayer = sanitizePlayer(msg.player);
410
+ const changed = nextPlayer === null ? hostPlayer !== null : hostPlayer === null || nextPlayer.name !== hostPlayer.name || nextPlayer.avatarUrl !== hostPlayer.avatarUrl;
411
+ hostPlayer = nextPlayer;
412
+ if (changed) {
413
+ const snapshot = nextPlayer ? { name: nextPlayer.name, avatarUrl: nextPlayer.avatarUrl } : null;
414
+ for (let i = 0; i < playerChangeHandlers.length; i++) {
415
+ safeCall(playerChangeHandlers[i], snapshot);
416
+ }
417
+ }
399
418
  }
400
419
  const rewardedAds = msg.rewardedAds;
401
420
  if (rewardedAds && typeof rewardedAds === "object") {
@@ -527,6 +546,7 @@ function createBBArcade() {
527
546
  let resolved = false;
528
547
  let status = "unavailable";
529
548
  let rewardOffered = false;
549
+ let adShown = false;
530
550
  const waitTimer = setTimeout(function() {
531
551
  if (rewardOffered || resolved) return;
532
552
  const timeout = Object.assign(
@@ -571,6 +591,7 @@ function createBBArcade() {
571
591
  }
572
592
  post("rewarded_ad", void 0, Object.assign({ event: "started" }, base));
573
593
  try {
594
+ adShown = true;
574
595
  showAdFn();
575
596
  } catch (err) {
576
597
  const showError = Object.assign(
@@ -620,7 +641,13 @@ function createBBArcade() {
620
641
  },
621
642
  adBreakDone: function(placementInfo) {
622
643
  if (resolved) return;
623
- const result = rewardOffered || status !== "unavailable" ? { status } : { status: "unavailable", error: "no_rewarded_ad" };
644
+ const result = rewardOffered || status !== "unavailable" ? {
645
+ // A reward was offered but the ad never reached the screen
646
+ // (the game retained show() and let the break lapse): the
647
+ // break is over without a view, so resolve 'dismissed',
648
+ // not the non-terminal-looking 'ready'.
649
+ status: status === "ready" && !adShown ? "dismissed" : status
650
+ } : { status: "unavailable", error: "no_rewarded_ad" };
624
651
  if (placementInfo && typeof placementInfo === "object") {
625
652
  result.breakStatus = cleanText(
626
653
  placementInfo.breakStatus
@@ -823,12 +850,25 @@ function createBBArcade() {
823
850
  prepareRewardedBreak: prepareRewardedAd,
824
851
  rewardedBreak,
825
852
  save: function(blob) {
826
- return request("save", String(blob));
853
+ const data = String(blob);
854
+ if (utf8ByteLength(data) > MAX_SAVE_BYTES) {
855
+ return Promise.reject(sdkError("too_large"));
856
+ }
857
+ return request("save", data);
827
858
  },
828
859
  load: function() {
829
860
  return request("load");
830
861
  },
831
862
  getPlayer,
863
+ onPlayerChange: function(handler) {
864
+ if (typeof handler !== "function") return function() {
865
+ };
866
+ playerChangeHandlers.push(handler);
867
+ return function() {
868
+ const idx = playerChangeHandlers.indexOf(handler);
869
+ if (idx !== -1) playerChangeHandlers.splice(idx, 1);
870
+ };
871
+ },
832
872
  getVariant: function(key, variants) {
833
873
  const sorted = Array.isArray(variants) ? variants.slice().sort() : [];
834
874
  const fallback = sorted.length > 0 ? sorted[0] : null;
@@ -895,6 +935,8 @@ function createInertBBArcade() {
895
935
  save: () => Promise.reject(sdkError2("unsupported")),
896
936
  load: () => Promise.reject(sdkError2("unsupported")),
897
937
  getPlayer: () => Promise.resolve(null),
938
+ onPlayerChange: () => () => {
939
+ },
898
940
  getVariant: (key, variants) => {
899
941
  const sorted = Array.isArray(variants) ? variants.slice().sort() : [];
900
942
  return Promise.resolve(sorted.length > 0 ? sorted[0] : null);
@@ -908,13 +950,17 @@ var BBArcade = typeof window === "undefined" ? createInertBBArcade() : createBBA
908
950
 
909
951
  // src/multiplayer.ts
910
952
  var WELCOME_TIMEOUT_MS = 1e4;
953
+ var MIN_WELCOME_TIMEOUT_MS = 1e3;
954
+ var MAX_WELCOME_TIMEOUT_MS = 6e4;
911
955
  var MAX_RECONNECT_ATTEMPTS = 3;
912
956
  var MAX_JOIN_DATA_BYTES = 1024;
957
+ var SEAT_FULL_CLOSE_CODE = 4005;
913
958
  var CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
914
959
  var CODE_LENGTH = 4;
915
- function sdkError3(code) {
960
+ function sdkError3(code, detail) {
916
961
  const err = new Error(code);
917
962
  err.code = code;
963
+ if (detail) err.detail = detail;
918
964
  return err;
919
965
  }
920
966
  function randomChars(length) {
@@ -931,23 +977,13 @@ function randomChars(length) {
931
977
  function generateCode() {
932
978
  return randomChars(CODE_LENGTH);
933
979
  }
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;
980
+ var cachedTextEncoder2 = null;
981
+ function utf8ByteLength2(value) {
982
+ if (typeof TextEncoder !== "undefined") {
983
+ cachedTextEncoder2 = cachedTextEncoder2 || new TextEncoder();
984
+ return cachedTextEncoder2.encode(value).length;
949
985
  }
950
- return bytes;
986
+ return new Blob([value]).size;
951
987
  }
952
988
  function serializeJoinData(data) {
953
989
  if (data === void 0) return null;
@@ -958,28 +994,34 @@ function serializeJoinData(data) {
958
994
  } catch (e) {
959
995
  throw sdkError3("rejected");
960
996
  }
961
- if (!serialized || utf8ByteLength(serialized) > MAX_JOIN_DATA_BYTES) {
997
+ if (!serialized || utf8ByteLength2(serialized) > MAX_JOIN_DATA_BYTES) {
962
998
  throw sdkError3("rejected");
963
999
  }
964
1000
  return serialized;
965
1001
  }
966
- function requestTicket(roomId) {
1002
+ function requestTicket(payload) {
967
1003
  const request = BBArcade._request;
968
1004
  if (typeof request !== "function") return Promise.reject(sdkError3("unsupported"));
969
- return request("mp_ticket", { roomId }).then((grant) => {
1005
+ return request("mp_ticket", payload).then((grant) => {
970
1006
  const g = grant;
971
1007
  if (!g || typeof g.ticket !== "string" || typeof g.roomUrl !== "string") {
972
1008
  throw sdkError3("error");
973
1009
  }
974
- return { ticket: g.ticket, roomUrl: g.roomUrl, expiresAt: Number(g.expiresAt) || 0 };
1010
+ return {
1011
+ ticket: g.ticket,
1012
+ roomUrl: g.roomUrl,
1013
+ expiresAt: Number(g.expiresAt) || 0,
1014
+ roomId: typeof g.roomId === "string" && g.roomId ? g.roomId : null
1015
+ };
975
1016
  });
976
1017
  }
977
1018
  var RoomConnection = class {
978
- constructor(code, devGrant, joinData) {
1019
+ constructor(code, devGrant, joinData, welcomeTimeoutMs = WELCOME_TIMEOUT_MS) {
979
1020
  this.playerId = "";
980
1021
  this.players = [];
981
1022
  this.state = null;
982
1023
  this.connected = false;
1024
+ this.latencyMs = null;
983
1025
  this.ws = null;
984
1026
  // Keyed by event name; values are erased to (unknown) => void internally and
985
1027
  // retyped at the on()/emit() boundary, which keeps the public API strictly
@@ -992,15 +1034,20 @@ var RoomConnection = class {
992
1034
  // or the welcome timed out — permanent outcomes that must not reconnect.
993
1035
  this.terminal = false;
994
1036
  this.reconnectAttempts = 0;
1037
+ // The in-flight connect()'s settlement, so leave() can reject a join that
1038
+ // is still waiting on its welcome instead of stranding it on the timeout.
1039
+ this.pendingConnect = null;
995
1040
  this.code = code;
996
1041
  this.devGrant = devGrant;
997
1042
  this.joinData = joinData;
1043
+ this.welcomeTimeoutMs = welcomeTimeoutMs;
998
1044
  }
999
1045
  /** Connect and resolve on the server's welcome (post-ticket-verification). */
1000
1046
  connect(grant) {
1001
1047
  return new Promise((resolve, reject) => {
1002
1048
  let settled = false;
1003
1049
  const url = grant.roomUrl + (grant.roomUrl.indexOf("?") === -1 ? "?" : "&") + "ticket=" + encodeURIComponent(grant.ticket) + (this.joinData === null ? "" : "&join=" + encodeURIComponent(this.joinData));
1050
+ const connectStartedAt = Date.now();
1004
1051
  let ws;
1005
1052
  try {
1006
1053
  ws = new WebSocket(url);
@@ -1011,13 +1058,15 @@ var RoomConnection = class {
1011
1058
  const welcomeTimer = setTimeout(() => {
1012
1059
  if (settled) return;
1013
1060
  settled = true;
1061
+ this.pendingConnect = null;
1014
1062
  this.terminal = true;
1015
1063
  try {
1016
1064
  ws.close();
1017
1065
  } catch (err) {
1018
1066
  }
1019
1067
  reject(sdkError3("error"));
1020
- }, WELCOME_TIMEOUT_MS);
1068
+ }, this.welcomeTimeoutMs);
1069
+ this.pendingConnect = { reject, timer: welcomeTimer };
1021
1070
  ws.onmessage = (event) => {
1022
1071
  var _a, _b, _c, _d, _e;
1023
1072
  let msg;
@@ -1028,14 +1077,23 @@ var RoomConnection = class {
1028
1077
  }
1029
1078
  if (!msg || typeof msg.t !== "string") return;
1030
1079
  if (msg.t === "welcome") {
1080
+ if (this.intentionalClose) {
1081
+ try {
1082
+ ws.close();
1083
+ } catch (err) {
1084
+ }
1085
+ return;
1086
+ }
1031
1087
  const reconnected = this.reconnectAttempts > 0;
1032
1088
  this.playerId = String((_a = msg.playerId) != null ? _a : "");
1033
1089
  this.players = Array.isArray(msg.players) ? msg.players : [];
1034
1090
  this.state = (_b = msg.state) != null ? _b : null;
1035
1091
  this.connected = true;
1036
1092
  this.reconnectAttempts = 0;
1093
+ this.latencyMs = Math.max(0, Date.now() - connectStartedAt);
1037
1094
  if (!settled) {
1038
1095
  settled = true;
1096
+ this.pendingConnect = null;
1039
1097
  clearTimeout(welcomeTimer);
1040
1098
  resolve(this);
1041
1099
  }
@@ -1077,21 +1135,24 @@ var RoomConnection = class {
1077
1135
  if (!settled) {
1078
1136
  settled = true;
1079
1137
  this.terminal = true;
1138
+ this.pendingConnect = null;
1080
1139
  clearTimeout(welcomeTimer);
1081
1140
  try {
1082
1141
  ws.close();
1083
1142
  } catch (err) {
1084
1143
  }
1085
- reject(sdkError3(code === "unauthenticated" ? "unauthenticated" : "rejected"));
1144
+ reject(sdkError3(code === "unauthenticated" ? "unauthenticated" : "rejected", code));
1086
1145
  return;
1087
1146
  }
1088
1147
  this.emit("error", { code });
1089
1148
  }
1090
1149
  };
1091
- ws.onclose = () => {
1150
+ ws.onclose = (event) => {
1092
1151
  this.connected = false;
1152
+ if (event && event.code === SEAT_FULL_CLOSE_CODE) this.terminal = true;
1093
1153
  if (!settled) {
1094
1154
  settled = true;
1155
+ this.pendingConnect = null;
1095
1156
  clearTimeout(welcomeTimer);
1096
1157
  reject(sdkError3("error"));
1097
1158
  return;
@@ -1120,7 +1181,7 @@ var RoomConnection = class {
1120
1181
  await new Promise((r) => setTimeout(r, 400 * this.reconnectAttempts));
1121
1182
  if (this.intentionalClose || this.ended || this.terminal) return;
1122
1183
  try {
1123
- const grant = (_a = this.devGrant) != null ? _a : await requestTicket(this.code);
1184
+ const grant = (_a = this.devGrant) != null ? _a : await requestTicket({ roomId: this.code });
1124
1185
  if (this.intentionalClose || this.ended || this.terminal) return;
1125
1186
  await this.connect(grant);
1126
1187
  if (this.intentionalClose) this.leave();
@@ -1155,6 +1216,12 @@ var RoomConnection = class {
1155
1216
  var _a;
1156
1217
  this.intentionalClose = true;
1157
1218
  this.connected = false;
1219
+ if (this.pendingConnect) {
1220
+ const pending = this.pendingConnect;
1221
+ this.pendingConnect = null;
1222
+ clearTimeout(pending.timer);
1223
+ pending.reject(sdkError3("error"));
1224
+ }
1158
1225
  try {
1159
1226
  (_a = this.ws) == null ? void 0 : _a.close(1e3);
1160
1227
  } catch (err) {
@@ -1174,23 +1241,57 @@ var RoomConnection = class {
1174
1241
  }
1175
1242
  }
1176
1243
  };
1244
+ var MATCH_ATTEMPTS = 3;
1245
+ function normalizeWelcomeTimeoutMs(value) {
1246
+ if (typeof value !== "number" || !isFinite(value)) return WELCOME_TIMEOUT_MS;
1247
+ return Math.min(MAX_WELCOME_TIMEOUT_MS, Math.max(MIN_WELCOME_TIMEOUT_MS, Math.floor(value)));
1248
+ }
1249
+ async function matchJoin(joinData, timeoutMs) {
1250
+ let lastError = sdkError3("error");
1251
+ for (let attempt = 0; attempt < MATCH_ATTEMPTS; attempt++) {
1252
+ const grant = await requestTicket({ matchmake: true });
1253
+ if (!grant.roomId) throw sdkError3("error");
1254
+ try {
1255
+ return await new RoomConnection(grant.roomId, null, joinData, timeoutMs).connect(grant);
1256
+ } catch (error) {
1257
+ lastError = error;
1258
+ const detail = error == null ? void 0 : error.detail;
1259
+ if (detail !== "room_full" && detail !== "room_over") throw error;
1260
+ }
1261
+ }
1262
+ throw lastError;
1263
+ }
1177
1264
  function joinRoom(options) {
1178
1265
  const opts = options != null ? options : {};
1179
1266
  if (typeof window === "undefined" || typeof WebSocket === "undefined") {
1180
1267
  return Promise.reject(sdkError3("unsupported"));
1181
1268
  }
1182
- const code = opts.create || !opts.code ? generateCode() : opts.code.toUpperCase();
1183
1269
  let joinData;
1184
1270
  try {
1185
1271
  joinData = serializeJoinData(opts.joinData);
1186
1272
  } catch (err) {
1187
1273
  return Promise.reject(err);
1188
1274
  }
1275
+ const timeoutMs = normalizeWelcomeTimeoutMs(opts.timeoutMs);
1276
+ if (opts.match) {
1277
+ if (opts.code || opts.create || opts.roomUrl || opts.ticket) {
1278
+ return Promise.reject(sdkError3("rejected"));
1279
+ }
1280
+ return matchJoin(joinData, timeoutMs);
1281
+ }
1282
+ const code = opts.create || !opts.code ? generateCode() : opts.code.toUpperCase();
1189
1283
  if (opts.roomUrl && opts.ticket) {
1190
- const devGrant = { ticket: opts.ticket, roomUrl: opts.roomUrl, expiresAt: 0 };
1191
- return new RoomConnection(code, devGrant, joinData).connect(devGrant);
1284
+ const devGrant = {
1285
+ ticket: opts.ticket,
1286
+ roomUrl: opts.roomUrl,
1287
+ expiresAt: 0,
1288
+ roomId: code
1289
+ };
1290
+ return new RoomConnection(code, devGrant, joinData, timeoutMs).connect(devGrant);
1192
1291
  }
1193
- return requestTicket(code).then((grant) => new RoomConnection(code, null, joinData).connect(grant));
1292
+ return requestTicket({ roomId: code }).then(
1293
+ (grant) => new RoomConnection(code, null, joinData, timeoutMs).connect(grant)
1294
+ );
1194
1295
  }
1195
1296
  var multiplayer = { joinRoom };
1196
1297
  // Annotate the CommonJS export names for ESM import in node: