@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.
@@ -3,6 +3,316 @@ var VERSION = 1;
3
3
  var SDK_MESSAGE_SOURCE = "bb-arcade";
4
4
  var HOST_MESSAGE_SOURCE = "bb-arcade-host";
5
5
 
6
+ // src/storage.ts
7
+ var FLUSH_DELAY_MS = 400;
8
+ var ENVELOPE_TAG = "__bbArcadeStorage";
9
+ var ENVELOPE_VERSION = 1;
10
+ function hasOwn(target, key) {
11
+ return Object.prototype.hasOwnProperty.call(target, key);
12
+ }
13
+ function emptyMap() {
14
+ return /* @__PURE__ */ Object.create(null);
15
+ }
16
+ function storageError(code) {
17
+ const err = new Error(code);
18
+ err.code = code;
19
+ return err;
20
+ }
21
+ function parseEnvelope(raw) {
22
+ if (raw == null) return { ls: {}, app: null };
23
+ if (raw.charAt(0) !== "{") return { ls: {}, app: raw };
24
+ let parsed;
25
+ try {
26
+ parsed = JSON.parse(raw);
27
+ } catch (err) {
28
+ return { ls: {}, app: raw };
29
+ }
30
+ if (!parsed || typeof parsed !== "object") return { ls: {}, app: raw };
31
+ const record = parsed;
32
+ if (record[ENVELOPE_TAG] !== ENVELOPE_VERSION) return { ls: {}, app: raw };
33
+ const ls = emptyMap();
34
+ const source = record.ls;
35
+ if (source && typeof source === "object") {
36
+ const entries = source;
37
+ for (const key in entries) {
38
+ if (hasOwn(entries, key) && typeof entries[key] === "string") {
39
+ ls[key] = entries[key];
40
+ }
41
+ }
42
+ }
43
+ return { ls, app: typeof record.app === "string" ? record.app : null };
44
+ }
45
+ function buildEnvelope(ls, app) {
46
+ const envelope = {};
47
+ envelope[ENVELOPE_TAG] = ENVELOPE_VERSION;
48
+ envelope.ls = ls;
49
+ envelope.app = app;
50
+ return JSON.stringify(envelope);
51
+ }
52
+ function quotaError() {
53
+ try {
54
+ return new DOMException("Cloud save quota exceeded", "QuotaExceededError");
55
+ } catch (err) {
56
+ const fallback = new Error("Cloud save quota exceeded");
57
+ fallback.name = "QuotaExceededError";
58
+ return fallback;
59
+ }
60
+ }
61
+ var RESERVED = {
62
+ getItem: true,
63
+ setItem: true,
64
+ removeItem: true,
65
+ clear: true,
66
+ key: true,
67
+ length: true
68
+ };
69
+ function createStorageShim(backend = {}) {
70
+ const map = emptyMap();
71
+ const localWrites = emptyMap();
72
+ function keys() {
73
+ return Object.keys(map);
74
+ }
75
+ function changed() {
76
+ if (backend.onChange) backend.onChange();
77
+ }
78
+ const api = {
79
+ getItem: function(key) {
80
+ const name = String(key);
81
+ return hasOwn(map, name) ? map[name] : null;
82
+ },
83
+ setItem: function(key, value) {
84
+ const name = String(key);
85
+ const next = String(value);
86
+ if (backend.wouldOverflow && backend.wouldOverflow(name, next)) throw quotaError();
87
+ map[name] = next;
88
+ localWrites[name] = true;
89
+ changed();
90
+ },
91
+ removeItem: function(key) {
92
+ const name = String(key);
93
+ localWrites[name] = true;
94
+ if (!hasOwn(map, name)) return;
95
+ delete map[name];
96
+ changed();
97
+ },
98
+ clear: function() {
99
+ const existing = keys();
100
+ for (let i = 0; i < existing.length; i++) {
101
+ delete map[existing[i]];
102
+ localWrites[existing[i]] = true;
103
+ }
104
+ changed();
105
+ },
106
+ key: function(index) {
107
+ const existing = keys();
108
+ const at = Number(index);
109
+ return at >= 0 && at < existing.length ? existing[at] : null;
110
+ },
111
+ get length() {
112
+ return keys().length;
113
+ }
114
+ };
115
+ const storage = new Proxy(api, {
116
+ get(target, prop, receiver) {
117
+ if (typeof prop !== "string") return Reflect.get(target, prop, receiver);
118
+ if (hasOwn(RESERVED, prop)) return Reflect.get(target, prop, receiver);
119
+ return hasOwn(map, prop) ? map[prop] : void 0;
120
+ },
121
+ set(target, prop, value) {
122
+ if (typeof prop !== "string" || hasOwn(RESERVED, prop)) return true;
123
+ api.setItem(prop, value);
124
+ return true;
125
+ },
126
+ has(target, prop) {
127
+ if (typeof prop !== "string") return Reflect.has(target, prop);
128
+ return hasOwn(RESERVED, prop) || hasOwn(map, prop);
129
+ },
130
+ deleteProperty(target, prop) {
131
+ if (typeof prop === "string" && !hasOwn(RESERVED, prop)) api.removeItem(prop);
132
+ return true;
133
+ },
134
+ ownKeys() {
135
+ return keys();
136
+ },
137
+ getOwnPropertyDescriptor(target, prop) {
138
+ if (typeof prop !== "string" || !hasOwn(map, prop)) return void 0;
139
+ return { value: map[prop], writable: true, enumerable: true, configurable: true };
140
+ }
141
+ });
142
+ return {
143
+ storage,
144
+ entries: function() {
145
+ const copy = emptyMap();
146
+ const existing = keys();
147
+ for (let i = 0; i < existing.length; i++) copy[existing[i]] = map[existing[i]];
148
+ return copy;
149
+ },
150
+ hydrate: function(values) {
151
+ for (const key in values) {
152
+ if (hasOwn(values, key) && !hasOwn(localWrites, key)) map[key] = values[key];
153
+ }
154
+ }
155
+ };
156
+ }
157
+ function settled(promise) {
158
+ return promise.then(
159
+ function() {
160
+ },
161
+ function() {
162
+ }
163
+ );
164
+ }
165
+ function installSessionStorageShim() {
166
+ if (nativeStorageWorks("sessionStorage")) return;
167
+ try {
168
+ Object.defineProperty(window, "sessionStorage", {
169
+ value: createStorageShim().storage,
170
+ configurable: true
171
+ });
172
+ } catch (err) {
173
+ }
174
+ }
175
+ function nativeStorageWorks(area) {
176
+ try {
177
+ const store = window[area];
178
+ if (!store) return false;
179
+ const probe = "__bb_arcade_probe__";
180
+ store.setItem(probe, "1");
181
+ store.removeItem(probe);
182
+ return true;
183
+ } catch (err) {
184
+ return false;
185
+ }
186
+ }
187
+ function createStorageController(options) {
188
+ let shim = null;
189
+ let mode = "native";
190
+ let appBlob = null;
191
+ let hydration = null;
192
+ let flushTimer = null;
193
+ let inFlight = Promise.resolve();
194
+ let dirty = false;
195
+ let hydrated = false;
196
+ function serialize() {
197
+ return buildEnvelope(shim ? shim.entries() : {}, appBlob);
198
+ }
199
+ function hydrateFromCloud() {
200
+ hydration = options.load().then(
201
+ function(raw) {
202
+ const envelope = parseEnvelope(raw);
203
+ if (shim) shim.hydrate(envelope.ls);
204
+ if (appBlob === null) appBlob = envelope.app;
205
+ hydrated = true;
206
+ return mode;
207
+ },
208
+ function(err) {
209
+ const code = err && err.code;
210
+ if (code === "unsupported" || code === "unauthenticated") mode = "memory";
211
+ return mode;
212
+ }
213
+ );
214
+ return hydration;
215
+ }
216
+ function wouldOverflow(key, value) {
217
+ if (!shim) return false;
218
+ const next = shim.entries();
219
+ next[key] = value;
220
+ return options.byteLength(buildEnvelope(next, appBlob)) > options.maxBytes;
221
+ }
222
+ function persist() {
223
+ return whenHydrated().then(function() {
224
+ if (mode === "memory") return;
225
+ if (hydrated) return options.save(serialize());
226
+ return hydrateFromCloud().then(function() {
227
+ if (mode === "memory") return;
228
+ if (!hydrated) throw storageError("error");
229
+ return options.save(serialize());
230
+ });
231
+ }).then(void 0, function(err) {
232
+ dirty = true;
233
+ throw err;
234
+ });
235
+ }
236
+ function flushNow() {
237
+ if (flushTimer !== null) {
238
+ clearTimeout(flushTimer);
239
+ flushTimer = null;
240
+ }
241
+ dirty = false;
242
+ const next = inFlight.then(persist, persist);
243
+ inFlight = settled(next);
244
+ return next;
245
+ }
246
+ function scheduleFlush() {
247
+ dirty = true;
248
+ if (flushTimer !== null) return;
249
+ flushTimer = setTimeout(function() {
250
+ flushTimer = null;
251
+ void settled(flushNow());
252
+ }, FLUSH_DELAY_MS);
253
+ }
254
+ function whenHydrated() {
255
+ return hydration ? hydration : Promise.resolve(mode);
256
+ }
257
+ function attachUnloadFlush() {
258
+ const flushIfDirty = function() {
259
+ if (dirty) void settled(flushNow());
260
+ };
261
+ try {
262
+ window.addEventListener("pagehide", flushIfDirty);
263
+ window.addEventListener("visibilitychange", function() {
264
+ if (document.visibilityState === "hidden") flushIfDirty();
265
+ });
266
+ } catch (err) {
267
+ }
268
+ }
269
+ function install() {
270
+ if (shim) return mode;
271
+ installSessionStorageShim();
272
+ if (nativeStorageWorks("localStorage")) {
273
+ mode = "native";
274
+ return mode;
275
+ }
276
+ const handle = createStorageShim({ onChange: scheduleFlush, wouldOverflow });
277
+ try {
278
+ Object.defineProperty(window, "localStorage", {
279
+ value: handle.storage,
280
+ configurable: true
281
+ });
282
+ } catch (err) {
283
+ return mode;
284
+ }
285
+ shim = handle;
286
+ mode = "cloud";
287
+ attachUnloadFlush();
288
+ void hydrateFromCloud();
289
+ return mode;
290
+ }
291
+ return {
292
+ install,
293
+ active: function() {
294
+ return shim !== null;
295
+ },
296
+ ready: function() {
297
+ return whenHydrated();
298
+ },
299
+ flush: function() {
300
+ if (!shim && appBlob === null) return Promise.resolve();
301
+ return flushNow();
302
+ },
303
+ writeApp: function(blob) {
304
+ if (options.byteLength(buildEnvelope(shim ? shim.entries() : {}, blob)) > options.maxBytes) {
305
+ return Promise.reject(storageError("too_large"));
306
+ }
307
+ appBlob = blob;
308
+ return flushNow();
309
+ },
310
+ get mode() {
311
+ return mode;
312
+ }
313
+ };
314
+ }
315
+
6
316
  // src/core.ts
7
317
  var BOUNTY_BOARD_URL = "https://www.bountyboard.gg/arcade";
8
318
  var ADSENSE_CLIENT_ID = "ca-pub-7727110228190547";
@@ -10,6 +320,8 @@ var AD_PLACEMENT_SRC = "https://pagead2.googlesyndication.com/pagead/js/adsbygoo
10
320
  var AD_PLACEMENT_SCRIPT_ID = "bb-arcade-h5-ads";
11
321
  var DEFAULT_AD_LOAD_TIMEOUT_MS = 6e3;
12
322
  var SAVE_TIMEOUT_MS = 15e3;
323
+ var LOCK_TIMEOUT_MS = 5e3;
324
+ var MAX_SAVE_BYTES = 1024 * 1024;
13
325
  var INIT_CONFIG_GRACE_MS = 1500;
14
326
  var DEFAULT_ALLOW = [
15
327
  "bountyboard.gg",
@@ -34,6 +346,14 @@ function cleanText(value, fallback, max) {
34
346
  if (typeof value !== "string") return fallback || "";
35
347
  return value.replace(/[^\w:.-]+/g, "_").slice(0, max || 80) || fallback || "";
36
348
  }
349
+ var cachedTextEncoder = null;
350
+ function utf8ByteLength(value) {
351
+ if (typeof TextEncoder !== "undefined") {
352
+ cachedTextEncoder = cachedTextEncoder || new TextEncoder();
353
+ return cachedTextEncoder.encode(value).length;
354
+ }
355
+ return new Blob([value]).size;
356
+ }
37
357
  function toBoolean(value) {
38
358
  return value === true || value === "true" || value === "1";
39
359
  }
@@ -153,13 +473,14 @@ function createBBArcade() {
153
473
  };
154
474
  let hostConfigReceived = false;
155
475
  let hostPlayer = null;
476
+ const playerChangeHandlers = [];
156
477
  let blocked = false;
157
478
  const pending = {};
158
479
  let seq = 0;
159
480
  function post(type, score, extra) {
160
481
  if (blocked) return;
161
482
  const message = { source: SDK_MESSAGE_SOURCE, v: VERSION, type };
162
- if (typeof score === "number" && isFinite(score)) message.score = Math.floor(score);
483
+ if (typeof score === "number" && isFinite(score)) message.score = Math.trunc(score);
163
484
  if (extra && typeof extra === "object") {
164
485
  const fields = extra;
165
486
  for (const key in fields) {
@@ -180,7 +501,7 @@ function createBBArcade() {
180
501
  return true;
181
502
  }
182
503
  }
183
- function request(type, payload) {
504
+ function request(type, payload, timeoutMs = SAVE_TIMEOUT_MS) {
184
505
  return new Promise(function(resolve, reject) {
185
506
  if (blocked) return reject(sdkError("error"));
186
507
  if (!isEmbedded()) return reject(sdkError("unsupported"));
@@ -190,7 +511,7 @@ function createBBArcade() {
190
511
  delete pending[requestId];
191
512
  reject(sdkError("error"));
192
513
  }
193
- }, SAVE_TIMEOUT_MS);
514
+ }, timeoutMs);
194
515
  pending[requestId] = { resolve, reject, timer };
195
516
  try {
196
517
  window.parent.postMessage(
@@ -282,7 +603,7 @@ function createBBArcade() {
282
603
  if (!hostnameAllowed(origin, allow)) blockHost(options);
283
604
  return;
284
605
  }
285
- request("host").then(
606
+ request("host", void 0, LOCK_TIMEOUT_MS).then(
286
607
  function(res) {
287
608
  const r = res;
288
609
  if (!hostnameAllowed(r && r.origin, allow)) blockHost(options);
@@ -306,7 +627,7 @@ function createBBArcade() {
306
627
  lockToHostBestEffort(opts, allow);
307
628
  return;
308
629
  }
309
- request("host_attest").then(
630
+ request("host_attest", void 0, LOCK_TIMEOUT_MS).then(
310
631
  function(res) {
311
632
  const r = res;
312
633
  if (!r || !r.attestation) {
@@ -368,7 +689,15 @@ function createBBArcade() {
368
689
  if (msg.source !== HOST_MESSAGE_SOURCE || msg.type !== "config") return;
369
690
  hostConfigReceived = true;
370
691
  if (msg.player !== void 0) {
371
- hostPlayer = sanitizePlayer(msg.player);
692
+ const nextPlayer = sanitizePlayer(msg.player);
693
+ const changed = nextPlayer === null ? hostPlayer !== null : hostPlayer === null || nextPlayer.name !== hostPlayer.name || nextPlayer.avatarUrl !== hostPlayer.avatarUrl;
694
+ hostPlayer = nextPlayer;
695
+ if (changed) {
696
+ const snapshot = nextPlayer ? { name: nextPlayer.name, avatarUrl: nextPlayer.avatarUrl } : null;
697
+ for (let i = 0; i < playerChangeHandlers.length; i++) {
698
+ safeCall(playerChangeHandlers[i], snapshot);
699
+ }
700
+ }
372
701
  }
373
702
  const rewardedAds = msg.rewardedAds;
374
703
  if (rewardedAds && typeof rewardedAds === "object") {
@@ -500,6 +829,7 @@ function createBBArcade() {
500
829
  let resolved = false;
501
830
  let status = "unavailable";
502
831
  let rewardOffered = false;
832
+ let adShown = false;
503
833
  const waitTimer = setTimeout(function() {
504
834
  if (rewardOffered || resolved) return;
505
835
  const timeout = Object.assign(
@@ -544,6 +874,7 @@ function createBBArcade() {
544
874
  }
545
875
  post("rewarded_ad", void 0, Object.assign({ event: "started" }, base));
546
876
  try {
877
+ adShown = true;
547
878
  showAdFn();
548
879
  } catch (err) {
549
880
  const showError = Object.assign(
@@ -593,7 +924,13 @@ function createBBArcade() {
593
924
  },
594
925
  adBreakDone: function(placementInfo) {
595
926
  if (resolved) return;
596
- const result = rewardOffered || status !== "unavailable" ? { status } : { status: "unavailable", error: "no_rewarded_ad" };
927
+ const result = rewardOffered || status !== "unavailable" ? {
928
+ // A reward was offered but the ad never reached the screen
929
+ // (the game retained show() and let the break lapse): the
930
+ // break is over without a view, so resolve 'dismissed',
931
+ // not the non-terminal-looking 'ready'.
932
+ status: status === "ready" && !adShown ? "dismissed" : status
933
+ } : { status: "unavailable", error: "no_rewarded_ad" };
597
934
  if (placementInfo && typeof placementInfo === "object") {
598
935
  result.breakStatus = cleanText(
599
936
  placementInfo.breakStatus
@@ -764,6 +1101,16 @@ function createBBArcade() {
764
1101
  if (!hostnameAllowed(event.origin, DEFAULT_ALLOW)) return;
765
1102
  applyHostConfig(event.data);
766
1103
  });
1104
+ const storage = createStorageController({
1105
+ save: function(blob) {
1106
+ return request("save", blob);
1107
+ },
1108
+ load: function() {
1109
+ return request("load");
1110
+ },
1111
+ byteLength: utf8ByteLength,
1112
+ maxBytes: MAX_SAVE_BYTES
1113
+ });
767
1114
  const sdk = {
768
1115
  lockToHost,
769
1116
  init,
@@ -796,12 +1143,29 @@ function createBBArcade() {
796
1143
  prepareRewardedBreak: prepareRewardedAd,
797
1144
  rewardedBreak,
798
1145
  save: function(blob) {
799
- return request("save", String(blob));
1146
+ const data = String(blob);
1147
+ if (utf8ByteLength(data) > MAX_SAVE_BYTES) {
1148
+ return Promise.reject(sdkError("too_large"));
1149
+ }
1150
+ if (storage.active()) return storage.writeApp(data);
1151
+ return request("save", data);
800
1152
  },
801
1153
  load: function() {
802
- return request("load");
1154
+ return request("load").then(function(raw) {
1155
+ return parseEnvelope(raw).app;
1156
+ });
803
1157
  },
1158
+ storage,
804
1159
  getPlayer,
1160
+ onPlayerChange: function(handler) {
1161
+ if (typeof handler !== "function") return function() {
1162
+ };
1163
+ playerChangeHandlers.push(handler);
1164
+ return function() {
1165
+ const idx = playerChangeHandlers.indexOf(handler);
1166
+ if (idx !== -1) playerChangeHandlers.splice(idx, 1);
1167
+ };
1168
+ },
805
1169
  getVariant: function(key, variants) {
806
1170
  const sorted = Array.isArray(variants) ? variants.slice().sort() : [];
807
1171
  const fallback = sorted.length > 0 ? sorted[0] : null;
@@ -867,7 +1231,17 @@ function createInertBBArcade() {
867
1231
  rewardedBreak: () => Promise.resolve(false),
868
1232
  save: () => Promise.reject(sdkError2("unsupported")),
869
1233
  load: () => Promise.reject(sdkError2("unsupported")),
1234
+ // No window to shim and nothing to persist to: report the honest mode
1235
+ // rather than pretending an install succeeded.
1236
+ storage: {
1237
+ install: () => "memory",
1238
+ ready: () => Promise.resolve("memory"),
1239
+ flush: () => Promise.resolve(),
1240
+ mode: "memory"
1241
+ },
870
1242
  getPlayer: () => Promise.resolve(null),
1243
+ onPlayerChange: () => () => {
1244
+ },
871
1245
  getVariant: (key, variants) => {
872
1246
  const sorted = Array.isArray(variants) ? variants.slice().sort() : [];
873
1247
  return Promise.resolve(sorted.length > 0 ? sorted[0] : null);
@@ -885,4 +1259,4 @@ export {
885
1259
  BBArcade,
886
1260
  src_default
887
1261
  };
888
- //# sourceMappingURL=chunk-OWOESH4X.js.map
1262
+ //# sourceMappingURL=chunk-PYW5F45R.js.map