@bountyboard/arcade-sdk 1.0.1 → 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,12 +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;
956
+ var MAX_JOIN_DATA_BYTES = 1024;
957
+ var SEAT_FULL_CLOSE_CODE = 4005;
912
958
  var CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
913
959
  var CODE_LENGTH = 4;
914
- function sdkError3(code) {
960
+ function sdkError3(code, detail) {
915
961
  const err = new Error(code);
916
962
  err.code = code;
963
+ if (detail) err.detail = detail;
917
964
  return err;
918
965
  }
919
966
  function randomChars(length) {
@@ -930,27 +977,51 @@ function randomChars(length) {
930
977
  function generateCode() {
931
978
  return randomChars(CODE_LENGTH);
932
979
  }
933
- var guestKey = null;
934
- function getGuestKey() {
935
- if (!guestKey) guestKey = randomChars(16);
936
- return guestKey;
980
+ var cachedTextEncoder2 = null;
981
+ function utf8ByteLength2(value) {
982
+ if (typeof TextEncoder !== "undefined") {
983
+ cachedTextEncoder2 = cachedTextEncoder2 || new TextEncoder();
984
+ return cachedTextEncoder2.encode(value).length;
985
+ }
986
+ return new Blob([value]).size;
937
987
  }
938
- function requestTicket(roomId) {
988
+ function serializeJoinData(data) {
989
+ if (data === void 0) return null;
990
+ if (!data || typeof data !== "object" || Array.isArray(data)) throw sdkError3("rejected");
991
+ let serialized;
992
+ try {
993
+ serialized = JSON.stringify(data);
994
+ } catch (e) {
995
+ throw sdkError3("rejected");
996
+ }
997
+ if (!serialized || utf8ByteLength2(serialized) > MAX_JOIN_DATA_BYTES) {
998
+ throw sdkError3("rejected");
999
+ }
1000
+ return serialized;
1001
+ }
1002
+ function requestTicket(payload) {
939
1003
  const request = BBArcade._request;
940
1004
  if (typeof request !== "function") return Promise.reject(sdkError3("unsupported"));
941
- return request("mp_ticket", { roomId, guestKey: getGuestKey() }).then((grant) => {
1005
+ return request("mp_ticket", payload).then((grant) => {
942
1006
  const g = grant;
943
1007
  if (!g || typeof g.ticket !== "string" || typeof g.roomUrl !== "string") {
944
1008
  throw sdkError3("error");
945
1009
  }
946
- 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
+ };
947
1016
  });
948
1017
  }
949
1018
  var RoomConnection = class {
950
- constructor(code, devGrant) {
1019
+ constructor(code, devGrant, joinData, welcomeTimeoutMs = WELCOME_TIMEOUT_MS) {
951
1020
  this.playerId = "";
952
1021
  this.players = [];
953
1022
  this.state = null;
1023
+ this.connected = false;
1024
+ this.latencyMs = null;
954
1025
  this.ws = null;
955
1026
  // Keyed by event name; values are erased to (unknown) => void internally and
956
1027
  // retyped at the on()/emit() boundary, which keeps the public API strictly
@@ -963,14 +1034,20 @@ var RoomConnection = class {
963
1034
  // or the welcome timed out — permanent outcomes that must not reconnect.
964
1035
  this.terminal = false;
965
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;
966
1040
  this.code = code;
967
1041
  this.devGrant = devGrant;
1042
+ this.joinData = joinData;
1043
+ this.welcomeTimeoutMs = welcomeTimeoutMs;
968
1044
  }
969
1045
  /** Connect and resolve on the server's welcome (post-ticket-verification). */
970
1046
  connect(grant) {
971
1047
  return new Promise((resolve, reject) => {
972
1048
  let settled = false;
973
- const url = grant.roomUrl + (grant.roomUrl.indexOf("?") === -1 ? "?" : "&") + "ticket=" + encodeURIComponent(grant.ticket);
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();
974
1051
  let ws;
975
1052
  try {
976
1053
  ws = new WebSocket(url);
@@ -981,13 +1058,15 @@ var RoomConnection = class {
981
1058
  const welcomeTimer = setTimeout(() => {
982
1059
  if (settled) return;
983
1060
  settled = true;
1061
+ this.pendingConnect = null;
984
1062
  this.terminal = true;
985
1063
  try {
986
1064
  ws.close();
987
1065
  } catch (err) {
988
1066
  }
989
1067
  reject(sdkError3("error"));
990
- }, WELCOME_TIMEOUT_MS);
1068
+ }, this.welcomeTimeoutMs);
1069
+ this.pendingConnect = { reject, timer: welcomeTimer };
991
1070
  ws.onmessage = (event) => {
992
1071
  var _a, _b, _c, _d, _e;
993
1072
  let msg;
@@ -998,15 +1077,27 @@ var RoomConnection = class {
998
1077
  }
999
1078
  if (!msg || typeof msg.t !== "string") return;
1000
1079
  if (msg.t === "welcome") {
1080
+ if (this.intentionalClose) {
1081
+ try {
1082
+ ws.close();
1083
+ } catch (err) {
1084
+ }
1085
+ return;
1086
+ }
1087
+ const reconnected = this.reconnectAttempts > 0;
1001
1088
  this.playerId = String((_a = msg.playerId) != null ? _a : "");
1002
1089
  this.players = Array.isArray(msg.players) ? msg.players : [];
1003
1090
  this.state = (_b = msg.state) != null ? _b : null;
1091
+ this.connected = true;
1004
1092
  this.reconnectAttempts = 0;
1093
+ this.latencyMs = Math.max(0, Date.now() - connectStartedAt);
1005
1094
  if (!settled) {
1006
1095
  settled = true;
1096
+ this.pendingConnect = null;
1007
1097
  clearTimeout(welcomeTimer);
1008
1098
  resolve(this);
1009
1099
  }
1100
+ if (reconnected) this.emit("connection", { connected: true, reconnecting: false });
1010
1101
  return;
1011
1102
  }
1012
1103
  if (msg.t === "snapshot") {
@@ -1044,25 +1135,30 @@ var RoomConnection = class {
1044
1135
  if (!settled) {
1045
1136
  settled = true;
1046
1137
  this.terminal = true;
1138
+ this.pendingConnect = null;
1047
1139
  clearTimeout(welcomeTimer);
1048
1140
  try {
1049
1141
  ws.close();
1050
1142
  } catch (err) {
1051
1143
  }
1052
- reject(sdkError3(code === "unauthenticated" ? "unauthenticated" : "rejected"));
1144
+ reject(sdkError3(code === "unauthenticated" ? "unauthenticated" : "rejected", code));
1053
1145
  return;
1054
1146
  }
1055
1147
  this.emit("error", { code });
1056
1148
  }
1057
1149
  };
1058
- ws.onclose = () => {
1150
+ ws.onclose = (event) => {
1151
+ this.connected = false;
1152
+ if (event && event.code === SEAT_FULL_CLOSE_CODE) this.terminal = true;
1059
1153
  if (!settled) {
1060
1154
  settled = true;
1155
+ this.pendingConnect = null;
1061
1156
  clearTimeout(welcomeTimer);
1062
1157
  reject(sdkError3("error"));
1063
1158
  return;
1064
1159
  }
1065
1160
  if (this.intentionalClose || this.ended || this.terminal) {
1161
+ this.emit("connection", { connected: false, reconnecting: false });
1066
1162
  this.emit("close", { reconnecting: false });
1067
1163
  return;
1068
1164
  }
@@ -1080,11 +1176,12 @@ var RoomConnection = class {
1080
1176
  return;
1081
1177
  }
1082
1178
  this.reconnectAttempts += 1;
1179
+ this.emit("connection", { connected: false, reconnecting: true });
1083
1180
  this.emit("close", { reconnecting: true });
1084
1181
  await new Promise((r) => setTimeout(r, 400 * this.reconnectAttempts));
1085
1182
  if (this.intentionalClose || this.ended || this.terminal) return;
1086
1183
  try {
1087
- const grant = (_a = this.devGrant) != null ? _a : await requestTicket(this.code);
1184
+ const grant = (_a = this.devGrant) != null ? _a : await requestTicket({ roomId: this.code });
1088
1185
  if (this.intentionalClose || this.ended || this.terminal) return;
1089
1186
  await this.connect(grant);
1090
1187
  if (this.intentionalClose) this.leave();
@@ -1093,11 +1190,16 @@ var RoomConnection = class {
1093
1190
  }
1094
1191
  }
1095
1192
  send(input) {
1096
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
1193
+ this.trySend(input);
1194
+ }
1195
+ trySend(input) {
1196
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return false;
1097
1197
  this.seq += 1;
1098
1198
  try {
1099
1199
  this.ws.send(JSON.stringify({ t: "input", seq: this.seq, data: input }));
1200
+ return true;
1100
1201
  } catch (err) {
1202
+ return false;
1101
1203
  }
1102
1204
  }
1103
1205
  on(event, handler) {
@@ -1113,6 +1215,13 @@ var RoomConnection = class {
1113
1215
  leave() {
1114
1216
  var _a;
1115
1217
  this.intentionalClose = true;
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
+ }
1116
1225
  try {
1117
1226
  (_a = this.ws) == null ? void 0 : _a.close(1e3);
1118
1227
  } catch (err) {
@@ -1132,17 +1241,57 @@ var RoomConnection = class {
1132
1241
  }
1133
1242
  }
1134
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
+ }
1135
1264
  function joinRoom(options) {
1136
1265
  const opts = options != null ? options : {};
1137
1266
  if (typeof window === "undefined" || typeof WebSocket === "undefined") {
1138
1267
  return Promise.reject(sdkError3("unsupported"));
1139
1268
  }
1269
+ let joinData;
1270
+ try {
1271
+ joinData = serializeJoinData(opts.joinData);
1272
+ } catch (err) {
1273
+ return Promise.reject(err);
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
+ }
1140
1282
  const code = opts.create || !opts.code ? generateCode() : opts.code.toUpperCase();
1141
1283
  if (opts.roomUrl && opts.ticket) {
1142
- const devGrant = { ticket: opts.ticket, roomUrl: opts.roomUrl, expiresAt: 0 };
1143
- return new RoomConnection(code, devGrant).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);
1144
1291
  }
1145
- return requestTicket(code).then((grant) => new RoomConnection(code, null).connect(grant));
1292
+ return requestTicket({ roomId: code }).then(
1293
+ (grant) => new RoomConnection(code, null, joinData, timeoutMs).connect(grant)
1294
+ );
1146
1295
  }
1147
1296
  var multiplayer = { joinRoom };
1148
1297
  // Annotate the CommonJS export names for ESM import in node: