@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.
package/dist/global.js CHANGED
@@ -12,6 +12,8 @@
12
12
  var AD_PLACEMENT_SCRIPT_ID = "bb-arcade-h5-ads";
13
13
  var DEFAULT_AD_LOAD_TIMEOUT_MS = 6e3;
14
14
  var SAVE_TIMEOUT_MS = 15e3;
15
+ var LOCK_TIMEOUT_MS = 5e3;
16
+ var MAX_SAVE_BYTES = 1024 * 1024;
15
17
  var INIT_CONFIG_GRACE_MS = 1500;
16
18
  var DEFAULT_ALLOW = [
17
19
  "bountyboard.gg",
@@ -36,6 +38,14 @@
36
38
  if (typeof value !== "string") return fallback || "";
37
39
  return value.replace(/[^\w:.-]+/g, "_").slice(0, max || 80) || fallback || "";
38
40
  }
41
+ var cachedTextEncoder = null;
42
+ function utf8ByteLength(value) {
43
+ if (typeof TextEncoder !== "undefined") {
44
+ cachedTextEncoder = cachedTextEncoder || new TextEncoder();
45
+ return cachedTextEncoder.encode(value).length;
46
+ }
47
+ return new Blob([value]).size;
48
+ }
39
49
  function toBoolean(value) {
40
50
  return value === true || value === "true" || value === "1";
41
51
  }
@@ -155,13 +165,14 @@
155
165
  };
156
166
  let hostConfigReceived = false;
157
167
  let hostPlayer = null;
168
+ const playerChangeHandlers = [];
158
169
  let blocked = false;
159
170
  const pending = {};
160
171
  let seq = 0;
161
172
  function post(type, score, extra) {
162
173
  if (blocked) return;
163
174
  const message = { source: SDK_MESSAGE_SOURCE, v: VERSION, type };
164
- if (typeof score === "number" && isFinite(score)) message.score = Math.floor(score);
175
+ if (typeof score === "number" && isFinite(score)) message.score = Math.trunc(score);
165
176
  if (extra && typeof extra === "object") {
166
177
  const fields = extra;
167
178
  for (const key in fields) {
@@ -182,7 +193,7 @@
182
193
  return true;
183
194
  }
184
195
  }
185
- function request(type, payload) {
196
+ function request(type, payload, timeoutMs = SAVE_TIMEOUT_MS) {
186
197
  return new Promise(function(resolve, reject) {
187
198
  if (blocked) return reject(sdkError("error"));
188
199
  if (!isEmbedded()) return reject(sdkError("unsupported"));
@@ -192,7 +203,7 @@
192
203
  delete pending[requestId];
193
204
  reject(sdkError("error"));
194
205
  }
195
- }, SAVE_TIMEOUT_MS);
206
+ }, timeoutMs);
196
207
  pending[requestId] = { resolve, reject, timer };
197
208
  try {
198
209
  window.parent.postMessage(
@@ -284,7 +295,7 @@
284
295
  if (!hostnameAllowed(origin, allow)) blockHost(options);
285
296
  return;
286
297
  }
287
- request("host").then(
298
+ request("host", void 0, LOCK_TIMEOUT_MS).then(
288
299
  function(res) {
289
300
  const r = res;
290
301
  if (!hostnameAllowed(r && r.origin, allow)) blockHost(options);
@@ -308,7 +319,7 @@
308
319
  lockToHostBestEffort(opts, allow);
309
320
  return;
310
321
  }
311
- request("host_attest").then(
322
+ request("host_attest", void 0, LOCK_TIMEOUT_MS).then(
312
323
  function(res) {
313
324
  const r = res;
314
325
  if (!r || !r.attestation) {
@@ -370,7 +381,15 @@
370
381
  if (msg.source !== HOST_MESSAGE_SOURCE || msg.type !== "config") return;
371
382
  hostConfigReceived = true;
372
383
  if (msg.player !== void 0) {
373
- hostPlayer = sanitizePlayer(msg.player);
384
+ const nextPlayer = sanitizePlayer(msg.player);
385
+ const changed = nextPlayer === null ? hostPlayer !== null : hostPlayer === null || nextPlayer.name !== hostPlayer.name || nextPlayer.avatarUrl !== hostPlayer.avatarUrl;
386
+ hostPlayer = nextPlayer;
387
+ if (changed) {
388
+ const snapshot = nextPlayer ? { name: nextPlayer.name, avatarUrl: nextPlayer.avatarUrl } : null;
389
+ for (let i = 0; i < playerChangeHandlers.length; i++) {
390
+ safeCall(playerChangeHandlers[i], snapshot);
391
+ }
392
+ }
374
393
  }
375
394
  const rewardedAds = msg.rewardedAds;
376
395
  if (rewardedAds && typeof rewardedAds === "object") {
@@ -502,6 +521,7 @@
502
521
  let resolved = false;
503
522
  let status = "unavailable";
504
523
  let rewardOffered = false;
524
+ let adShown = false;
505
525
  const waitTimer = setTimeout(function() {
506
526
  if (rewardOffered || resolved) return;
507
527
  const timeout = Object.assign(
@@ -546,6 +566,7 @@
546
566
  }
547
567
  post("rewarded_ad", void 0, Object.assign({ event: "started" }, base));
548
568
  try {
569
+ adShown = true;
549
570
  showAdFn();
550
571
  } catch (err) {
551
572
  const showError = Object.assign(
@@ -595,7 +616,13 @@
595
616
  },
596
617
  adBreakDone: function(placementInfo) {
597
618
  if (resolved) return;
598
- const result = rewardOffered || status !== "unavailable" ? { status } : { status: "unavailable", error: "no_rewarded_ad" };
619
+ const result = rewardOffered || status !== "unavailable" ? {
620
+ // A reward was offered but the ad never reached the screen
621
+ // (the game retained show() and let the break lapse): the
622
+ // break is over without a view, so resolve 'dismissed',
623
+ // not the non-terminal-looking 'ready'.
624
+ status: status === "ready" && !adShown ? "dismissed" : status
625
+ } : { status: "unavailable", error: "no_rewarded_ad" };
599
626
  if (placementInfo && typeof placementInfo === "object") {
600
627
  result.breakStatus = cleanText(
601
628
  placementInfo.breakStatus
@@ -798,12 +825,25 @@
798
825
  prepareRewardedBreak: prepareRewardedAd,
799
826
  rewardedBreak,
800
827
  save: function(blob) {
801
- return request("save", String(blob));
828
+ const data = String(blob);
829
+ if (utf8ByteLength(data) > MAX_SAVE_BYTES) {
830
+ return Promise.reject(sdkError("too_large"));
831
+ }
832
+ return request("save", data);
802
833
  },
803
834
  load: function() {
804
835
  return request("load");
805
836
  },
806
837
  getPlayer,
838
+ onPlayerChange: function(handler) {
839
+ if (typeof handler !== "function") return function() {
840
+ };
841
+ playerChangeHandlers.push(handler);
842
+ return function() {
843
+ const idx = playerChangeHandlers.indexOf(handler);
844
+ if (idx !== -1) playerChangeHandlers.splice(idx, 1);
845
+ };
846
+ },
807
847
  getVariant: function(key, variants) {
808
848
  const sorted = Array.isArray(variants) ? variants.slice().sort() : [];
809
849
  const fallback = sorted.length > 0 ? sorted[0] : null;
@@ -870,6 +910,8 @@
870
910
  save: () => Promise.reject(sdkError2("unsupported")),
871
911
  load: () => Promise.reject(sdkError2("unsupported")),
872
912
  getPlayer: () => Promise.resolve(null),
913
+ onPlayerChange: () => () => {
914
+ },
873
915
  getVariant: (key, variants) => {
874
916
  const sorted = Array.isArray(variants) ? variants.slice().sort() : [];
875
917
  return Promise.resolve(sorted.length > 0 ? sorted[0] : null);
@@ -883,12 +925,17 @@
883
925
 
884
926
  // src/multiplayer.ts
885
927
  var WELCOME_TIMEOUT_MS = 1e4;
928
+ var MIN_WELCOME_TIMEOUT_MS = 1e3;
929
+ var MAX_WELCOME_TIMEOUT_MS = 6e4;
886
930
  var MAX_RECONNECT_ATTEMPTS = 3;
931
+ var MAX_JOIN_DATA_BYTES = 1024;
932
+ var SEAT_FULL_CLOSE_CODE = 4005;
887
933
  var CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
888
934
  var CODE_LENGTH = 4;
889
- function sdkError3(code) {
935
+ function sdkError3(code, detail) {
890
936
  const err = new Error(code);
891
937
  err.code = code;
938
+ if (detail) err.detail = detail;
892
939
  return err;
893
940
  }
894
941
  function randomChars(length) {
@@ -905,27 +952,51 @@
905
952
  function generateCode() {
906
953
  return randomChars(CODE_LENGTH);
907
954
  }
908
- var guestKey = null;
909
- function getGuestKey() {
910
- if (!guestKey) guestKey = randomChars(16);
911
- return guestKey;
955
+ var cachedTextEncoder2 = null;
956
+ function utf8ByteLength2(value) {
957
+ if (typeof TextEncoder !== "undefined") {
958
+ cachedTextEncoder2 = cachedTextEncoder2 || new TextEncoder();
959
+ return cachedTextEncoder2.encode(value).length;
960
+ }
961
+ return new Blob([value]).size;
912
962
  }
913
- function requestTicket(roomId) {
963
+ function serializeJoinData(data) {
964
+ if (data === void 0) return null;
965
+ if (!data || typeof data !== "object" || Array.isArray(data)) throw sdkError3("rejected");
966
+ let serialized;
967
+ try {
968
+ serialized = JSON.stringify(data);
969
+ } catch (e) {
970
+ throw sdkError3("rejected");
971
+ }
972
+ if (!serialized || utf8ByteLength2(serialized) > MAX_JOIN_DATA_BYTES) {
973
+ throw sdkError3("rejected");
974
+ }
975
+ return serialized;
976
+ }
977
+ function requestTicket(payload) {
914
978
  const request = BBArcade._request;
915
979
  if (typeof request !== "function") return Promise.reject(sdkError3("unsupported"));
916
- return request("mp_ticket", { roomId, guestKey: getGuestKey() }).then((grant) => {
980
+ return request("mp_ticket", payload).then((grant) => {
917
981
  const g = grant;
918
982
  if (!g || typeof g.ticket !== "string" || typeof g.roomUrl !== "string") {
919
983
  throw sdkError3("error");
920
984
  }
921
- return { ticket: g.ticket, roomUrl: g.roomUrl, expiresAt: Number(g.expiresAt) || 0 };
985
+ return {
986
+ ticket: g.ticket,
987
+ roomUrl: g.roomUrl,
988
+ expiresAt: Number(g.expiresAt) || 0,
989
+ roomId: typeof g.roomId === "string" && g.roomId ? g.roomId : null
990
+ };
922
991
  });
923
992
  }
924
993
  var RoomConnection = class {
925
- constructor(code, devGrant) {
994
+ constructor(code, devGrant, joinData, welcomeTimeoutMs = WELCOME_TIMEOUT_MS) {
926
995
  this.playerId = "";
927
996
  this.players = [];
928
997
  this.state = null;
998
+ this.connected = false;
999
+ this.latencyMs = null;
929
1000
  this.ws = null;
930
1001
  // Keyed by event name; values are erased to (unknown) => void internally and
931
1002
  // retyped at the on()/emit() boundary, which keeps the public API strictly
@@ -938,14 +1009,20 @@
938
1009
  // or the welcome timed out — permanent outcomes that must not reconnect.
939
1010
  this.terminal = false;
940
1011
  this.reconnectAttempts = 0;
1012
+ // The in-flight connect()'s settlement, so leave() can reject a join that
1013
+ // is still waiting on its welcome instead of stranding it on the timeout.
1014
+ this.pendingConnect = null;
941
1015
  this.code = code;
942
1016
  this.devGrant = devGrant;
1017
+ this.joinData = joinData;
1018
+ this.welcomeTimeoutMs = welcomeTimeoutMs;
943
1019
  }
944
1020
  /** Connect and resolve on the server's welcome (post-ticket-verification). */
945
1021
  connect(grant) {
946
1022
  return new Promise((resolve, reject) => {
947
1023
  let settled = false;
948
- const url = grant.roomUrl + (grant.roomUrl.indexOf("?") === -1 ? "?" : "&") + "ticket=" + encodeURIComponent(grant.ticket);
1024
+ const url = grant.roomUrl + (grant.roomUrl.indexOf("?") === -1 ? "?" : "&") + "ticket=" + encodeURIComponent(grant.ticket) + (this.joinData === null ? "" : "&join=" + encodeURIComponent(this.joinData));
1025
+ const connectStartedAt = Date.now();
949
1026
  let ws;
950
1027
  try {
951
1028
  ws = new WebSocket(url);
@@ -956,13 +1033,15 @@
956
1033
  const welcomeTimer = setTimeout(() => {
957
1034
  if (settled) return;
958
1035
  settled = true;
1036
+ this.pendingConnect = null;
959
1037
  this.terminal = true;
960
1038
  try {
961
1039
  ws.close();
962
1040
  } catch (err) {
963
1041
  }
964
1042
  reject(sdkError3("error"));
965
- }, WELCOME_TIMEOUT_MS);
1043
+ }, this.welcomeTimeoutMs);
1044
+ this.pendingConnect = { reject, timer: welcomeTimer };
966
1045
  ws.onmessage = (event) => {
967
1046
  var _a, _b, _c, _d, _e;
968
1047
  let msg;
@@ -973,15 +1052,27 @@
973
1052
  }
974
1053
  if (!msg || typeof msg.t !== "string") return;
975
1054
  if (msg.t === "welcome") {
1055
+ if (this.intentionalClose) {
1056
+ try {
1057
+ ws.close();
1058
+ } catch (err) {
1059
+ }
1060
+ return;
1061
+ }
1062
+ const reconnected = this.reconnectAttempts > 0;
976
1063
  this.playerId = String((_a = msg.playerId) != null ? _a : "");
977
1064
  this.players = Array.isArray(msg.players) ? msg.players : [];
978
1065
  this.state = (_b = msg.state) != null ? _b : null;
1066
+ this.connected = true;
979
1067
  this.reconnectAttempts = 0;
1068
+ this.latencyMs = Math.max(0, Date.now() - connectStartedAt);
980
1069
  if (!settled) {
981
1070
  settled = true;
1071
+ this.pendingConnect = null;
982
1072
  clearTimeout(welcomeTimer);
983
1073
  resolve(this);
984
1074
  }
1075
+ if (reconnected) this.emit("connection", { connected: true, reconnecting: false });
985
1076
  return;
986
1077
  }
987
1078
  if (msg.t === "snapshot") {
@@ -1019,25 +1110,30 @@
1019
1110
  if (!settled) {
1020
1111
  settled = true;
1021
1112
  this.terminal = true;
1113
+ this.pendingConnect = null;
1022
1114
  clearTimeout(welcomeTimer);
1023
1115
  try {
1024
1116
  ws.close();
1025
1117
  } catch (err) {
1026
1118
  }
1027
- reject(sdkError3(code === "unauthenticated" ? "unauthenticated" : "rejected"));
1119
+ reject(sdkError3(code === "unauthenticated" ? "unauthenticated" : "rejected", code));
1028
1120
  return;
1029
1121
  }
1030
1122
  this.emit("error", { code });
1031
1123
  }
1032
1124
  };
1033
- ws.onclose = () => {
1125
+ ws.onclose = (event) => {
1126
+ this.connected = false;
1127
+ if (event && event.code === SEAT_FULL_CLOSE_CODE) this.terminal = true;
1034
1128
  if (!settled) {
1035
1129
  settled = true;
1130
+ this.pendingConnect = null;
1036
1131
  clearTimeout(welcomeTimer);
1037
1132
  reject(sdkError3("error"));
1038
1133
  return;
1039
1134
  }
1040
1135
  if (this.intentionalClose || this.ended || this.terminal) {
1136
+ this.emit("connection", { connected: false, reconnecting: false });
1041
1137
  this.emit("close", { reconnecting: false });
1042
1138
  return;
1043
1139
  }
@@ -1055,11 +1151,12 @@
1055
1151
  return;
1056
1152
  }
1057
1153
  this.reconnectAttempts += 1;
1154
+ this.emit("connection", { connected: false, reconnecting: true });
1058
1155
  this.emit("close", { reconnecting: true });
1059
1156
  await new Promise((r) => setTimeout(r, 400 * this.reconnectAttempts));
1060
1157
  if (this.intentionalClose || this.ended || this.terminal) return;
1061
1158
  try {
1062
- const grant = (_a = this.devGrant) != null ? _a : await requestTicket(this.code);
1159
+ const grant = (_a = this.devGrant) != null ? _a : await requestTicket({ roomId: this.code });
1063
1160
  if (this.intentionalClose || this.ended || this.terminal) return;
1064
1161
  await this.connect(grant);
1065
1162
  if (this.intentionalClose) this.leave();
@@ -1068,11 +1165,16 @@
1068
1165
  }
1069
1166
  }
1070
1167
  send(input) {
1071
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
1168
+ this.trySend(input);
1169
+ }
1170
+ trySend(input) {
1171
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return false;
1072
1172
  this.seq += 1;
1073
1173
  try {
1074
1174
  this.ws.send(JSON.stringify({ t: "input", seq: this.seq, data: input }));
1175
+ return true;
1075
1176
  } catch (err) {
1177
+ return false;
1076
1178
  }
1077
1179
  }
1078
1180
  on(event, handler) {
@@ -1088,6 +1190,13 @@
1088
1190
  leave() {
1089
1191
  var _a;
1090
1192
  this.intentionalClose = true;
1193
+ this.connected = false;
1194
+ if (this.pendingConnect) {
1195
+ const pending = this.pendingConnect;
1196
+ this.pendingConnect = null;
1197
+ clearTimeout(pending.timer);
1198
+ pending.reject(sdkError3("error"));
1199
+ }
1091
1200
  try {
1092
1201
  (_a = this.ws) == null ? void 0 : _a.close(1e3);
1093
1202
  } catch (err) {
@@ -1107,17 +1216,57 @@
1107
1216
  }
1108
1217
  }
1109
1218
  };
1219
+ var MATCH_ATTEMPTS = 3;
1220
+ function normalizeWelcomeTimeoutMs(value) {
1221
+ if (typeof value !== "number" || !isFinite(value)) return WELCOME_TIMEOUT_MS;
1222
+ return Math.min(MAX_WELCOME_TIMEOUT_MS, Math.max(MIN_WELCOME_TIMEOUT_MS, Math.floor(value)));
1223
+ }
1224
+ async function matchJoin(joinData, timeoutMs) {
1225
+ let lastError = sdkError3("error");
1226
+ for (let attempt = 0; attempt < MATCH_ATTEMPTS; attempt++) {
1227
+ const grant = await requestTicket({ matchmake: true });
1228
+ if (!grant.roomId) throw sdkError3("error");
1229
+ try {
1230
+ return await new RoomConnection(grant.roomId, null, joinData, timeoutMs).connect(grant);
1231
+ } catch (error) {
1232
+ lastError = error;
1233
+ const detail = error == null ? void 0 : error.detail;
1234
+ if (detail !== "room_full" && detail !== "room_over") throw error;
1235
+ }
1236
+ }
1237
+ throw lastError;
1238
+ }
1110
1239
  function joinRoom(options) {
1111
1240
  const opts = options != null ? options : {};
1112
1241
  if (typeof window === "undefined" || typeof WebSocket === "undefined") {
1113
1242
  return Promise.reject(sdkError3("unsupported"));
1114
1243
  }
1244
+ let joinData;
1245
+ try {
1246
+ joinData = serializeJoinData(opts.joinData);
1247
+ } catch (err) {
1248
+ return Promise.reject(err);
1249
+ }
1250
+ const timeoutMs = normalizeWelcomeTimeoutMs(opts.timeoutMs);
1251
+ if (opts.match) {
1252
+ if (opts.code || opts.create || opts.roomUrl || opts.ticket) {
1253
+ return Promise.reject(sdkError3("rejected"));
1254
+ }
1255
+ return matchJoin(joinData, timeoutMs);
1256
+ }
1115
1257
  const code = opts.create || !opts.code ? generateCode() : opts.code.toUpperCase();
1116
1258
  if (opts.roomUrl && opts.ticket) {
1117
- const devGrant = { ticket: opts.ticket, roomUrl: opts.roomUrl, expiresAt: 0 };
1118
- return new RoomConnection(code, devGrant).connect(devGrant);
1259
+ const devGrant = {
1260
+ ticket: opts.ticket,
1261
+ roomUrl: opts.roomUrl,
1262
+ expiresAt: 0,
1263
+ roomId: code
1264
+ };
1265
+ return new RoomConnection(code, devGrant, joinData, timeoutMs).connect(devGrant);
1119
1266
  }
1120
- return requestTicket(code).then((grant) => new RoomConnection(code, null).connect(grant));
1267
+ return requestTicket({ roomId: code }).then(
1268
+ (grant) => new RoomConnection(code, null, joinData, timeoutMs).connect(grant)
1269
+ );
1121
1270
  }
1122
1271
  var multiplayer = { joinRoom };
1123
1272
 
package/dist/index.cjs CHANGED
@@ -38,6 +38,8 @@ var AD_PLACEMENT_SRC = "https://pagead2.googlesyndication.com/pagead/js/adsbygoo
38
38
  var AD_PLACEMENT_SCRIPT_ID = "bb-arcade-h5-ads";
39
39
  var DEFAULT_AD_LOAD_TIMEOUT_MS = 6e3;
40
40
  var SAVE_TIMEOUT_MS = 15e3;
41
+ var LOCK_TIMEOUT_MS = 5e3;
42
+ var MAX_SAVE_BYTES = 1024 * 1024;
41
43
  var INIT_CONFIG_GRACE_MS = 1500;
42
44
  var DEFAULT_ALLOW = [
43
45
  "bountyboard.gg",
@@ -62,6 +64,14 @@ function cleanText(value, fallback, max) {
62
64
  if (typeof value !== "string") return fallback || "";
63
65
  return value.replace(/[^\w:.-]+/g, "_").slice(0, max || 80) || fallback || "";
64
66
  }
67
+ var cachedTextEncoder = null;
68
+ function utf8ByteLength(value) {
69
+ if (typeof TextEncoder !== "undefined") {
70
+ cachedTextEncoder = cachedTextEncoder || new TextEncoder();
71
+ return cachedTextEncoder.encode(value).length;
72
+ }
73
+ return new Blob([value]).size;
74
+ }
65
75
  function toBoolean(value) {
66
76
  return value === true || value === "true" || value === "1";
67
77
  }
@@ -181,13 +191,14 @@ function createBBArcade() {
181
191
  };
182
192
  let hostConfigReceived = false;
183
193
  let hostPlayer = null;
194
+ const playerChangeHandlers = [];
184
195
  let blocked = false;
185
196
  const pending = {};
186
197
  let seq = 0;
187
198
  function post(type, score, extra) {
188
199
  if (blocked) return;
189
200
  const message = { source: SDK_MESSAGE_SOURCE, v: VERSION, type };
190
- if (typeof score === "number" && isFinite(score)) message.score = Math.floor(score);
201
+ if (typeof score === "number" && isFinite(score)) message.score = Math.trunc(score);
191
202
  if (extra && typeof extra === "object") {
192
203
  const fields = extra;
193
204
  for (const key in fields) {
@@ -208,7 +219,7 @@ function createBBArcade() {
208
219
  return true;
209
220
  }
210
221
  }
211
- function request(type, payload) {
222
+ function request(type, payload, timeoutMs = SAVE_TIMEOUT_MS) {
212
223
  return new Promise(function(resolve, reject) {
213
224
  if (blocked) return reject(sdkError("error"));
214
225
  if (!isEmbedded()) return reject(sdkError("unsupported"));
@@ -218,7 +229,7 @@ function createBBArcade() {
218
229
  delete pending[requestId];
219
230
  reject(sdkError("error"));
220
231
  }
221
- }, SAVE_TIMEOUT_MS);
232
+ }, timeoutMs);
222
233
  pending[requestId] = { resolve, reject, timer };
223
234
  try {
224
235
  window.parent.postMessage(
@@ -310,7 +321,7 @@ function createBBArcade() {
310
321
  if (!hostnameAllowed(origin, allow)) blockHost(options);
311
322
  return;
312
323
  }
313
- request("host").then(
324
+ request("host", void 0, LOCK_TIMEOUT_MS).then(
314
325
  function(res) {
315
326
  const r = res;
316
327
  if (!hostnameAllowed(r && r.origin, allow)) blockHost(options);
@@ -334,7 +345,7 @@ function createBBArcade() {
334
345
  lockToHostBestEffort(opts, allow);
335
346
  return;
336
347
  }
337
- request("host_attest").then(
348
+ request("host_attest", void 0, LOCK_TIMEOUT_MS).then(
338
349
  function(res) {
339
350
  const r = res;
340
351
  if (!r || !r.attestation) {
@@ -396,7 +407,15 @@ function createBBArcade() {
396
407
  if (msg.source !== HOST_MESSAGE_SOURCE || msg.type !== "config") return;
397
408
  hostConfigReceived = true;
398
409
  if (msg.player !== void 0) {
399
- hostPlayer = sanitizePlayer(msg.player);
410
+ const nextPlayer = sanitizePlayer(msg.player);
411
+ const changed = nextPlayer === null ? hostPlayer !== null : hostPlayer === null || nextPlayer.name !== hostPlayer.name || nextPlayer.avatarUrl !== hostPlayer.avatarUrl;
412
+ hostPlayer = nextPlayer;
413
+ if (changed) {
414
+ const snapshot = nextPlayer ? { name: nextPlayer.name, avatarUrl: nextPlayer.avatarUrl } : null;
415
+ for (let i = 0; i < playerChangeHandlers.length; i++) {
416
+ safeCall(playerChangeHandlers[i], snapshot);
417
+ }
418
+ }
400
419
  }
401
420
  const rewardedAds = msg.rewardedAds;
402
421
  if (rewardedAds && typeof rewardedAds === "object") {
@@ -528,6 +547,7 @@ function createBBArcade() {
528
547
  let resolved = false;
529
548
  let status = "unavailable";
530
549
  let rewardOffered = false;
550
+ let adShown = false;
531
551
  const waitTimer = setTimeout(function() {
532
552
  if (rewardOffered || resolved) return;
533
553
  const timeout = Object.assign(
@@ -572,6 +592,7 @@ function createBBArcade() {
572
592
  }
573
593
  post("rewarded_ad", void 0, Object.assign({ event: "started" }, base));
574
594
  try {
595
+ adShown = true;
575
596
  showAdFn();
576
597
  } catch (err) {
577
598
  const showError = Object.assign(
@@ -621,7 +642,13 @@ function createBBArcade() {
621
642
  },
622
643
  adBreakDone: function(placementInfo) {
623
644
  if (resolved) return;
624
- const result = rewardOffered || status !== "unavailable" ? { status } : { status: "unavailable", error: "no_rewarded_ad" };
645
+ const result = rewardOffered || status !== "unavailable" ? {
646
+ // A reward was offered but the ad never reached the screen
647
+ // (the game retained show() and let the break lapse): the
648
+ // break is over without a view, so resolve 'dismissed',
649
+ // not the non-terminal-looking 'ready'.
650
+ status: status === "ready" && !adShown ? "dismissed" : status
651
+ } : { status: "unavailable", error: "no_rewarded_ad" };
625
652
  if (placementInfo && typeof placementInfo === "object") {
626
653
  result.breakStatus = cleanText(
627
654
  placementInfo.breakStatus
@@ -824,12 +851,25 @@ function createBBArcade() {
824
851
  prepareRewardedBreak: prepareRewardedAd,
825
852
  rewardedBreak,
826
853
  save: function(blob) {
827
- return request("save", String(blob));
854
+ const data = String(blob);
855
+ if (utf8ByteLength(data) > MAX_SAVE_BYTES) {
856
+ return Promise.reject(sdkError("too_large"));
857
+ }
858
+ return request("save", data);
828
859
  },
829
860
  load: function() {
830
861
  return request("load");
831
862
  },
832
863
  getPlayer,
864
+ onPlayerChange: function(handler) {
865
+ if (typeof handler !== "function") return function() {
866
+ };
867
+ playerChangeHandlers.push(handler);
868
+ return function() {
869
+ const idx = playerChangeHandlers.indexOf(handler);
870
+ if (idx !== -1) playerChangeHandlers.splice(idx, 1);
871
+ };
872
+ },
833
873
  getVariant: function(key, variants) {
834
874
  const sorted = Array.isArray(variants) ? variants.slice().sort() : [];
835
875
  const fallback = sorted.length > 0 ? sorted[0] : null;
@@ -896,6 +936,8 @@ function createInertBBArcade() {
896
936
  save: () => Promise.reject(sdkError2("unsupported")),
897
937
  load: () => Promise.reject(sdkError2("unsupported")),
898
938
  getPlayer: () => Promise.resolve(null),
939
+ onPlayerChange: () => () => {
940
+ },
899
941
  getVariant: (key, variants) => {
900
942
  const sorted = Array.isArray(variants) ? variants.slice().sort() : [];
901
943
  return Promise.resolve(sorted.length > 0 ? sorted[0] : null);