@bountyboard/arcade-sdk 1.1.0 → 1.3.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/index.cjs CHANGED
@@ -31,6 +31,316 @@ var VERSION = 1;
31
31
  var SDK_MESSAGE_SOURCE = "bb-arcade";
32
32
  var HOST_MESSAGE_SOURCE = "bb-arcade-host";
33
33
 
34
+ // src/storage.ts
35
+ var FLUSH_DELAY_MS = 400;
36
+ var ENVELOPE_TAG = "__bbArcadeStorage";
37
+ var ENVELOPE_VERSION = 1;
38
+ function hasOwn(target, key) {
39
+ return Object.prototype.hasOwnProperty.call(target, key);
40
+ }
41
+ function emptyMap() {
42
+ return /* @__PURE__ */ Object.create(null);
43
+ }
44
+ function storageError(code) {
45
+ const err = new Error(code);
46
+ err.code = code;
47
+ return err;
48
+ }
49
+ function parseEnvelope(raw) {
50
+ if (raw == null) return { ls: {}, app: null };
51
+ if (raw.charAt(0) !== "{") return { ls: {}, app: raw };
52
+ let parsed;
53
+ try {
54
+ parsed = JSON.parse(raw);
55
+ } catch (err) {
56
+ return { ls: {}, app: raw };
57
+ }
58
+ if (!parsed || typeof parsed !== "object") return { ls: {}, app: raw };
59
+ const record = parsed;
60
+ if (record[ENVELOPE_TAG] !== ENVELOPE_VERSION) return { ls: {}, app: raw };
61
+ const ls = emptyMap();
62
+ const source = record.ls;
63
+ if (source && typeof source === "object") {
64
+ const entries = source;
65
+ for (const key in entries) {
66
+ if (hasOwn(entries, key) && typeof entries[key] === "string") {
67
+ ls[key] = entries[key];
68
+ }
69
+ }
70
+ }
71
+ return { ls, app: typeof record.app === "string" ? record.app : null };
72
+ }
73
+ function buildEnvelope(ls, app) {
74
+ const envelope = {};
75
+ envelope[ENVELOPE_TAG] = ENVELOPE_VERSION;
76
+ envelope.ls = ls;
77
+ envelope.app = app;
78
+ return JSON.stringify(envelope);
79
+ }
80
+ function quotaError() {
81
+ try {
82
+ return new DOMException("Cloud save quota exceeded", "QuotaExceededError");
83
+ } catch (err) {
84
+ const fallback = new Error("Cloud save quota exceeded");
85
+ fallback.name = "QuotaExceededError";
86
+ return fallback;
87
+ }
88
+ }
89
+ var RESERVED = {
90
+ getItem: true,
91
+ setItem: true,
92
+ removeItem: true,
93
+ clear: true,
94
+ key: true,
95
+ length: true
96
+ };
97
+ function createStorageShim(backend = {}) {
98
+ const map = emptyMap();
99
+ const localWrites = emptyMap();
100
+ function keys() {
101
+ return Object.keys(map);
102
+ }
103
+ function changed() {
104
+ if (backend.onChange) backend.onChange();
105
+ }
106
+ const api = {
107
+ getItem: function(key) {
108
+ const name = String(key);
109
+ return hasOwn(map, name) ? map[name] : null;
110
+ },
111
+ setItem: function(key, value) {
112
+ const name = String(key);
113
+ const next = String(value);
114
+ if (backend.wouldOverflow && backend.wouldOverflow(name, next)) throw quotaError();
115
+ map[name] = next;
116
+ localWrites[name] = true;
117
+ changed();
118
+ },
119
+ removeItem: function(key) {
120
+ const name = String(key);
121
+ localWrites[name] = true;
122
+ if (!hasOwn(map, name)) return;
123
+ delete map[name];
124
+ changed();
125
+ },
126
+ clear: function() {
127
+ const existing = keys();
128
+ for (let i = 0; i < existing.length; i++) {
129
+ delete map[existing[i]];
130
+ localWrites[existing[i]] = true;
131
+ }
132
+ changed();
133
+ },
134
+ key: function(index) {
135
+ const existing = keys();
136
+ const at = Number(index);
137
+ return at >= 0 && at < existing.length ? existing[at] : null;
138
+ },
139
+ get length() {
140
+ return keys().length;
141
+ }
142
+ };
143
+ const storage = new Proxy(api, {
144
+ get(target, prop, receiver) {
145
+ if (typeof prop !== "string") return Reflect.get(target, prop, receiver);
146
+ if (hasOwn(RESERVED, prop)) return Reflect.get(target, prop, receiver);
147
+ return hasOwn(map, prop) ? map[prop] : void 0;
148
+ },
149
+ set(target, prop, value) {
150
+ if (typeof prop !== "string" || hasOwn(RESERVED, prop)) return true;
151
+ api.setItem(prop, value);
152
+ return true;
153
+ },
154
+ has(target, prop) {
155
+ if (typeof prop !== "string") return Reflect.has(target, prop);
156
+ return hasOwn(RESERVED, prop) || hasOwn(map, prop);
157
+ },
158
+ deleteProperty(target, prop) {
159
+ if (typeof prop === "string" && !hasOwn(RESERVED, prop)) api.removeItem(prop);
160
+ return true;
161
+ },
162
+ ownKeys() {
163
+ return keys();
164
+ },
165
+ getOwnPropertyDescriptor(target, prop) {
166
+ if (typeof prop !== "string" || !hasOwn(map, prop)) return void 0;
167
+ return { value: map[prop], writable: true, enumerable: true, configurable: true };
168
+ }
169
+ });
170
+ return {
171
+ storage,
172
+ entries: function() {
173
+ const copy = emptyMap();
174
+ const existing = keys();
175
+ for (let i = 0; i < existing.length; i++) copy[existing[i]] = map[existing[i]];
176
+ return copy;
177
+ },
178
+ hydrate: function(values) {
179
+ for (const key in values) {
180
+ if (hasOwn(values, key) && !hasOwn(localWrites, key)) map[key] = values[key];
181
+ }
182
+ }
183
+ };
184
+ }
185
+ function settled(promise) {
186
+ return promise.then(
187
+ function() {
188
+ },
189
+ function() {
190
+ }
191
+ );
192
+ }
193
+ function installSessionStorageShim() {
194
+ if (nativeStorageWorks("sessionStorage")) return;
195
+ try {
196
+ Object.defineProperty(window, "sessionStorage", {
197
+ value: createStorageShim().storage,
198
+ configurable: true
199
+ });
200
+ } catch (err) {
201
+ }
202
+ }
203
+ function nativeStorageWorks(area) {
204
+ try {
205
+ const store = window[area];
206
+ if (!store) return false;
207
+ const probe = "__bb_arcade_probe__";
208
+ store.setItem(probe, "1");
209
+ store.removeItem(probe);
210
+ return true;
211
+ } catch (err) {
212
+ return false;
213
+ }
214
+ }
215
+ function createStorageController(options) {
216
+ let shim = null;
217
+ let mode = "native";
218
+ let appBlob = null;
219
+ let hydration = null;
220
+ let flushTimer = null;
221
+ let inFlight = Promise.resolve();
222
+ let dirty = false;
223
+ let hydrated = false;
224
+ function serialize() {
225
+ return buildEnvelope(shim ? shim.entries() : {}, appBlob);
226
+ }
227
+ function hydrateFromCloud() {
228
+ hydration = options.load().then(
229
+ function(raw) {
230
+ const envelope = parseEnvelope(raw);
231
+ if (shim) shim.hydrate(envelope.ls);
232
+ if (appBlob === null) appBlob = envelope.app;
233
+ hydrated = true;
234
+ return mode;
235
+ },
236
+ function(err) {
237
+ const code = err && err.code;
238
+ if (code === "unsupported" || code === "unauthenticated") mode = "memory";
239
+ return mode;
240
+ }
241
+ );
242
+ return hydration;
243
+ }
244
+ function wouldOverflow(key, value) {
245
+ if (!shim) return false;
246
+ const next = shim.entries();
247
+ next[key] = value;
248
+ return options.byteLength(buildEnvelope(next, appBlob)) > options.maxBytes;
249
+ }
250
+ function persist() {
251
+ return whenHydrated().then(function() {
252
+ if (mode === "memory") return;
253
+ if (hydrated) return options.save(serialize());
254
+ return hydrateFromCloud().then(function() {
255
+ if (mode === "memory") return;
256
+ if (!hydrated) throw storageError("error");
257
+ return options.save(serialize());
258
+ });
259
+ }).then(void 0, function(err) {
260
+ dirty = true;
261
+ throw err;
262
+ });
263
+ }
264
+ function flushNow() {
265
+ if (flushTimer !== null) {
266
+ clearTimeout(flushTimer);
267
+ flushTimer = null;
268
+ }
269
+ dirty = false;
270
+ const next = inFlight.then(persist, persist);
271
+ inFlight = settled(next);
272
+ return next;
273
+ }
274
+ function scheduleFlush() {
275
+ dirty = true;
276
+ if (flushTimer !== null) return;
277
+ flushTimer = setTimeout(function() {
278
+ flushTimer = null;
279
+ void settled(flushNow());
280
+ }, FLUSH_DELAY_MS);
281
+ }
282
+ function whenHydrated() {
283
+ return hydration ? hydration : Promise.resolve(mode);
284
+ }
285
+ function attachUnloadFlush() {
286
+ const flushIfDirty = function() {
287
+ if (dirty) void settled(flushNow());
288
+ };
289
+ try {
290
+ window.addEventListener("pagehide", flushIfDirty);
291
+ window.addEventListener("visibilitychange", function() {
292
+ if (document.visibilityState === "hidden") flushIfDirty();
293
+ });
294
+ } catch (err) {
295
+ }
296
+ }
297
+ function install() {
298
+ if (shim) return mode;
299
+ installSessionStorageShim();
300
+ if (nativeStorageWorks("localStorage")) {
301
+ mode = "native";
302
+ return mode;
303
+ }
304
+ const handle = createStorageShim({ onChange: scheduleFlush, wouldOverflow });
305
+ try {
306
+ Object.defineProperty(window, "localStorage", {
307
+ value: handle.storage,
308
+ configurable: true
309
+ });
310
+ } catch (err) {
311
+ return mode;
312
+ }
313
+ shim = handle;
314
+ mode = "cloud";
315
+ attachUnloadFlush();
316
+ void hydrateFromCloud();
317
+ return mode;
318
+ }
319
+ return {
320
+ install,
321
+ active: function() {
322
+ return shim !== null;
323
+ },
324
+ ready: function() {
325
+ return whenHydrated();
326
+ },
327
+ flush: function() {
328
+ if (!shim && appBlob === null) return Promise.resolve();
329
+ return flushNow();
330
+ },
331
+ writeApp: function(blob) {
332
+ if (options.byteLength(buildEnvelope(shim ? shim.entries() : {}, blob)) > options.maxBytes) {
333
+ return Promise.reject(storageError("too_large"));
334
+ }
335
+ appBlob = blob;
336
+ return flushNow();
337
+ },
338
+ get mode() {
339
+ return mode;
340
+ }
341
+ };
342
+ }
343
+
34
344
  // src/core.ts
35
345
  var BOUNTY_BOARD_URL = "https://www.bountyboard.gg/arcade";
36
346
  var ADSENSE_CLIENT_ID = "ca-pub-7727110228190547";
@@ -38,6 +348,8 @@ var AD_PLACEMENT_SRC = "https://pagead2.googlesyndication.com/pagead/js/adsbygoo
38
348
  var AD_PLACEMENT_SCRIPT_ID = "bb-arcade-h5-ads";
39
349
  var DEFAULT_AD_LOAD_TIMEOUT_MS = 6e3;
40
350
  var SAVE_TIMEOUT_MS = 15e3;
351
+ var LOCK_TIMEOUT_MS = 5e3;
352
+ var MAX_SAVE_BYTES = 1024 * 1024;
41
353
  var INIT_CONFIG_GRACE_MS = 1500;
42
354
  var DEFAULT_ALLOW = [
43
355
  "bountyboard.gg",
@@ -62,6 +374,14 @@ function cleanText(value, fallback, max) {
62
374
  if (typeof value !== "string") return fallback || "";
63
375
  return value.replace(/[^\w:.-]+/g, "_").slice(0, max || 80) || fallback || "";
64
376
  }
377
+ var cachedTextEncoder = null;
378
+ function utf8ByteLength(value) {
379
+ if (typeof TextEncoder !== "undefined") {
380
+ cachedTextEncoder = cachedTextEncoder || new TextEncoder();
381
+ return cachedTextEncoder.encode(value).length;
382
+ }
383
+ return new Blob([value]).size;
384
+ }
65
385
  function toBoolean(value) {
66
386
  return value === true || value === "true" || value === "1";
67
387
  }
@@ -181,13 +501,14 @@ function createBBArcade() {
181
501
  };
182
502
  let hostConfigReceived = false;
183
503
  let hostPlayer = null;
504
+ const playerChangeHandlers = [];
184
505
  let blocked = false;
185
506
  const pending = {};
186
507
  let seq = 0;
187
508
  function post(type, score, extra) {
188
509
  if (blocked) return;
189
510
  const message = { source: SDK_MESSAGE_SOURCE, v: VERSION, type };
190
- if (typeof score === "number" && isFinite(score)) message.score = Math.floor(score);
511
+ if (typeof score === "number" && isFinite(score)) message.score = Math.trunc(score);
191
512
  if (extra && typeof extra === "object") {
192
513
  const fields = extra;
193
514
  for (const key in fields) {
@@ -208,7 +529,7 @@ function createBBArcade() {
208
529
  return true;
209
530
  }
210
531
  }
211
- function request(type, payload) {
532
+ function request(type, payload, timeoutMs = SAVE_TIMEOUT_MS) {
212
533
  return new Promise(function(resolve, reject) {
213
534
  if (blocked) return reject(sdkError("error"));
214
535
  if (!isEmbedded()) return reject(sdkError("unsupported"));
@@ -218,7 +539,7 @@ function createBBArcade() {
218
539
  delete pending[requestId];
219
540
  reject(sdkError("error"));
220
541
  }
221
- }, SAVE_TIMEOUT_MS);
542
+ }, timeoutMs);
222
543
  pending[requestId] = { resolve, reject, timer };
223
544
  try {
224
545
  window.parent.postMessage(
@@ -310,7 +631,7 @@ function createBBArcade() {
310
631
  if (!hostnameAllowed(origin, allow)) blockHost(options);
311
632
  return;
312
633
  }
313
- request("host").then(
634
+ request("host", void 0, LOCK_TIMEOUT_MS).then(
314
635
  function(res) {
315
636
  const r = res;
316
637
  if (!hostnameAllowed(r && r.origin, allow)) blockHost(options);
@@ -334,7 +655,7 @@ function createBBArcade() {
334
655
  lockToHostBestEffort(opts, allow);
335
656
  return;
336
657
  }
337
- request("host_attest").then(
658
+ request("host_attest", void 0, LOCK_TIMEOUT_MS).then(
338
659
  function(res) {
339
660
  const r = res;
340
661
  if (!r || !r.attestation) {
@@ -396,7 +717,15 @@ function createBBArcade() {
396
717
  if (msg.source !== HOST_MESSAGE_SOURCE || msg.type !== "config") return;
397
718
  hostConfigReceived = true;
398
719
  if (msg.player !== void 0) {
399
- hostPlayer = sanitizePlayer(msg.player);
720
+ const nextPlayer = sanitizePlayer(msg.player);
721
+ const changed = nextPlayer === null ? hostPlayer !== null : hostPlayer === null || nextPlayer.name !== hostPlayer.name || nextPlayer.avatarUrl !== hostPlayer.avatarUrl;
722
+ hostPlayer = nextPlayer;
723
+ if (changed) {
724
+ const snapshot = nextPlayer ? { name: nextPlayer.name, avatarUrl: nextPlayer.avatarUrl } : null;
725
+ for (let i = 0; i < playerChangeHandlers.length; i++) {
726
+ safeCall(playerChangeHandlers[i], snapshot);
727
+ }
728
+ }
400
729
  }
401
730
  const rewardedAds = msg.rewardedAds;
402
731
  if (rewardedAds && typeof rewardedAds === "object") {
@@ -528,6 +857,7 @@ function createBBArcade() {
528
857
  let resolved = false;
529
858
  let status = "unavailable";
530
859
  let rewardOffered = false;
860
+ let adShown = false;
531
861
  const waitTimer = setTimeout(function() {
532
862
  if (rewardOffered || resolved) return;
533
863
  const timeout = Object.assign(
@@ -572,6 +902,7 @@ function createBBArcade() {
572
902
  }
573
903
  post("rewarded_ad", void 0, Object.assign({ event: "started" }, base));
574
904
  try {
905
+ adShown = true;
575
906
  showAdFn();
576
907
  } catch (err) {
577
908
  const showError = Object.assign(
@@ -621,7 +952,13 @@ function createBBArcade() {
621
952
  },
622
953
  adBreakDone: function(placementInfo) {
623
954
  if (resolved) return;
624
- const result = rewardOffered || status !== "unavailable" ? { status } : { status: "unavailable", error: "no_rewarded_ad" };
955
+ const result = rewardOffered || status !== "unavailable" ? {
956
+ // A reward was offered but the ad never reached the screen
957
+ // (the game retained show() and let the break lapse): the
958
+ // break is over without a view, so resolve 'dismissed',
959
+ // not the non-terminal-looking 'ready'.
960
+ status: status === "ready" && !adShown ? "dismissed" : status
961
+ } : { status: "unavailable", error: "no_rewarded_ad" };
625
962
  if (placementInfo && typeof placementInfo === "object") {
626
963
  result.breakStatus = cleanText(
627
964
  placementInfo.breakStatus
@@ -792,6 +1129,16 @@ function createBBArcade() {
792
1129
  if (!hostnameAllowed(event.origin, DEFAULT_ALLOW)) return;
793
1130
  applyHostConfig(event.data);
794
1131
  });
1132
+ const storage = createStorageController({
1133
+ save: function(blob) {
1134
+ return request("save", blob);
1135
+ },
1136
+ load: function() {
1137
+ return request("load");
1138
+ },
1139
+ byteLength: utf8ByteLength,
1140
+ maxBytes: MAX_SAVE_BYTES
1141
+ });
795
1142
  const sdk = {
796
1143
  lockToHost,
797
1144
  init,
@@ -824,12 +1171,29 @@ function createBBArcade() {
824
1171
  prepareRewardedBreak: prepareRewardedAd,
825
1172
  rewardedBreak,
826
1173
  save: function(blob) {
827
- return request("save", String(blob));
1174
+ const data = String(blob);
1175
+ if (utf8ByteLength(data) > MAX_SAVE_BYTES) {
1176
+ return Promise.reject(sdkError("too_large"));
1177
+ }
1178
+ if (storage.active()) return storage.writeApp(data);
1179
+ return request("save", data);
828
1180
  },
829
1181
  load: function() {
830
- return request("load");
1182
+ return request("load").then(function(raw) {
1183
+ return parseEnvelope(raw).app;
1184
+ });
831
1185
  },
1186
+ storage,
832
1187
  getPlayer,
1188
+ onPlayerChange: function(handler) {
1189
+ if (typeof handler !== "function") return function() {
1190
+ };
1191
+ playerChangeHandlers.push(handler);
1192
+ return function() {
1193
+ const idx = playerChangeHandlers.indexOf(handler);
1194
+ if (idx !== -1) playerChangeHandlers.splice(idx, 1);
1195
+ };
1196
+ },
833
1197
  getVariant: function(key, variants) {
834
1198
  const sorted = Array.isArray(variants) ? variants.slice().sort() : [];
835
1199
  const fallback = sorted.length > 0 ? sorted[0] : null;
@@ -895,7 +1259,17 @@ function createInertBBArcade() {
895
1259
  rewardedBreak: () => Promise.resolve(false),
896
1260
  save: () => Promise.reject(sdkError2("unsupported")),
897
1261
  load: () => Promise.reject(sdkError2("unsupported")),
1262
+ // No window to shim and nothing to persist to: report the honest mode
1263
+ // rather than pretending an install succeeded.
1264
+ storage: {
1265
+ install: () => "memory",
1266
+ ready: () => Promise.resolve("memory"),
1267
+ flush: () => Promise.resolve(),
1268
+ mode: "memory"
1269
+ },
898
1270
  getPlayer: () => Promise.resolve(null),
1271
+ onPlayerChange: () => () => {
1272
+ },
899
1273
  getVariant: (key, variants) => {
900
1274
  const sorted = Array.isArray(variants) ? variants.slice().sort() : [];
901
1275
  return Promise.resolve(sorted.length > 0 ? sorted[0] : null);