@bountyboard/arcade-sdk 1.2.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.
@@ -30,6 +30,316 @@ var VERSION = 1;
30
30
  var SDK_MESSAGE_SOURCE = "bb-arcade";
31
31
  var HOST_MESSAGE_SOURCE = "bb-arcade-host";
32
32
 
33
+ // src/storage.ts
34
+ var FLUSH_DELAY_MS = 400;
35
+ var ENVELOPE_TAG = "__bbArcadeStorage";
36
+ var ENVELOPE_VERSION = 1;
37
+ function hasOwn(target, key) {
38
+ return Object.prototype.hasOwnProperty.call(target, key);
39
+ }
40
+ function emptyMap() {
41
+ return /* @__PURE__ */ Object.create(null);
42
+ }
43
+ function storageError(code) {
44
+ const err = new Error(code);
45
+ err.code = code;
46
+ return err;
47
+ }
48
+ function parseEnvelope(raw) {
49
+ if (raw == null) return { ls: {}, app: null };
50
+ if (raw.charAt(0) !== "{") return { ls: {}, app: raw };
51
+ let parsed;
52
+ try {
53
+ parsed = JSON.parse(raw);
54
+ } catch (err) {
55
+ return { ls: {}, app: raw };
56
+ }
57
+ if (!parsed || typeof parsed !== "object") return { ls: {}, app: raw };
58
+ const record = parsed;
59
+ if (record[ENVELOPE_TAG] !== ENVELOPE_VERSION) return { ls: {}, app: raw };
60
+ const ls = emptyMap();
61
+ const source = record.ls;
62
+ if (source && typeof source === "object") {
63
+ const entries = source;
64
+ for (const key in entries) {
65
+ if (hasOwn(entries, key) && typeof entries[key] === "string") {
66
+ ls[key] = entries[key];
67
+ }
68
+ }
69
+ }
70
+ return { ls, app: typeof record.app === "string" ? record.app : null };
71
+ }
72
+ function buildEnvelope(ls, app) {
73
+ const envelope = {};
74
+ envelope[ENVELOPE_TAG] = ENVELOPE_VERSION;
75
+ envelope.ls = ls;
76
+ envelope.app = app;
77
+ return JSON.stringify(envelope);
78
+ }
79
+ function quotaError() {
80
+ try {
81
+ return new DOMException("Cloud save quota exceeded", "QuotaExceededError");
82
+ } catch (err) {
83
+ const fallback = new Error("Cloud save quota exceeded");
84
+ fallback.name = "QuotaExceededError";
85
+ return fallback;
86
+ }
87
+ }
88
+ var RESERVED = {
89
+ getItem: true,
90
+ setItem: true,
91
+ removeItem: true,
92
+ clear: true,
93
+ key: true,
94
+ length: true
95
+ };
96
+ function createStorageShim(backend = {}) {
97
+ const map = emptyMap();
98
+ const localWrites = emptyMap();
99
+ function keys() {
100
+ return Object.keys(map);
101
+ }
102
+ function changed() {
103
+ if (backend.onChange) backend.onChange();
104
+ }
105
+ const api = {
106
+ getItem: function(key) {
107
+ const name = String(key);
108
+ return hasOwn(map, name) ? map[name] : null;
109
+ },
110
+ setItem: function(key, value) {
111
+ const name = String(key);
112
+ const next = String(value);
113
+ if (backend.wouldOverflow && backend.wouldOverflow(name, next)) throw quotaError();
114
+ map[name] = next;
115
+ localWrites[name] = true;
116
+ changed();
117
+ },
118
+ removeItem: function(key) {
119
+ const name = String(key);
120
+ localWrites[name] = true;
121
+ if (!hasOwn(map, name)) return;
122
+ delete map[name];
123
+ changed();
124
+ },
125
+ clear: function() {
126
+ const existing = keys();
127
+ for (let i = 0; i < existing.length; i++) {
128
+ delete map[existing[i]];
129
+ localWrites[existing[i]] = true;
130
+ }
131
+ changed();
132
+ },
133
+ key: function(index) {
134
+ const existing = keys();
135
+ const at = Number(index);
136
+ return at >= 0 && at < existing.length ? existing[at] : null;
137
+ },
138
+ get length() {
139
+ return keys().length;
140
+ }
141
+ };
142
+ const storage = new Proxy(api, {
143
+ get(target, prop, receiver) {
144
+ if (typeof prop !== "string") return Reflect.get(target, prop, receiver);
145
+ if (hasOwn(RESERVED, prop)) return Reflect.get(target, prop, receiver);
146
+ return hasOwn(map, prop) ? map[prop] : void 0;
147
+ },
148
+ set(target, prop, value) {
149
+ if (typeof prop !== "string" || hasOwn(RESERVED, prop)) return true;
150
+ api.setItem(prop, value);
151
+ return true;
152
+ },
153
+ has(target, prop) {
154
+ if (typeof prop !== "string") return Reflect.has(target, prop);
155
+ return hasOwn(RESERVED, prop) || hasOwn(map, prop);
156
+ },
157
+ deleteProperty(target, prop) {
158
+ if (typeof prop === "string" && !hasOwn(RESERVED, prop)) api.removeItem(prop);
159
+ return true;
160
+ },
161
+ ownKeys() {
162
+ return keys();
163
+ },
164
+ getOwnPropertyDescriptor(target, prop) {
165
+ if (typeof prop !== "string" || !hasOwn(map, prop)) return void 0;
166
+ return { value: map[prop], writable: true, enumerable: true, configurable: true };
167
+ }
168
+ });
169
+ return {
170
+ storage,
171
+ entries: function() {
172
+ const copy = emptyMap();
173
+ const existing = keys();
174
+ for (let i = 0; i < existing.length; i++) copy[existing[i]] = map[existing[i]];
175
+ return copy;
176
+ },
177
+ hydrate: function(values) {
178
+ for (const key in values) {
179
+ if (hasOwn(values, key) && !hasOwn(localWrites, key)) map[key] = values[key];
180
+ }
181
+ }
182
+ };
183
+ }
184
+ function settled(promise) {
185
+ return promise.then(
186
+ function() {
187
+ },
188
+ function() {
189
+ }
190
+ );
191
+ }
192
+ function installSessionStorageShim() {
193
+ if (nativeStorageWorks("sessionStorage")) return;
194
+ try {
195
+ Object.defineProperty(window, "sessionStorage", {
196
+ value: createStorageShim().storage,
197
+ configurable: true
198
+ });
199
+ } catch (err) {
200
+ }
201
+ }
202
+ function nativeStorageWorks(area) {
203
+ try {
204
+ const store = window[area];
205
+ if (!store) return false;
206
+ const probe = "__bb_arcade_probe__";
207
+ store.setItem(probe, "1");
208
+ store.removeItem(probe);
209
+ return true;
210
+ } catch (err) {
211
+ return false;
212
+ }
213
+ }
214
+ function createStorageController(options) {
215
+ let shim = null;
216
+ let mode = "native";
217
+ let appBlob = null;
218
+ let hydration = null;
219
+ let flushTimer = null;
220
+ let inFlight = Promise.resolve();
221
+ let dirty = false;
222
+ let hydrated = false;
223
+ function serialize() {
224
+ return buildEnvelope(shim ? shim.entries() : {}, appBlob);
225
+ }
226
+ function hydrateFromCloud() {
227
+ hydration = options.load().then(
228
+ function(raw) {
229
+ const envelope = parseEnvelope(raw);
230
+ if (shim) shim.hydrate(envelope.ls);
231
+ if (appBlob === null) appBlob = envelope.app;
232
+ hydrated = true;
233
+ return mode;
234
+ },
235
+ function(err) {
236
+ const code = err && err.code;
237
+ if (code === "unsupported" || code === "unauthenticated") mode = "memory";
238
+ return mode;
239
+ }
240
+ );
241
+ return hydration;
242
+ }
243
+ function wouldOverflow(key, value) {
244
+ if (!shim) return false;
245
+ const next = shim.entries();
246
+ next[key] = value;
247
+ return options.byteLength(buildEnvelope(next, appBlob)) > options.maxBytes;
248
+ }
249
+ function persist() {
250
+ return whenHydrated().then(function() {
251
+ if (mode === "memory") return;
252
+ if (hydrated) return options.save(serialize());
253
+ return hydrateFromCloud().then(function() {
254
+ if (mode === "memory") return;
255
+ if (!hydrated) throw storageError("error");
256
+ return options.save(serialize());
257
+ });
258
+ }).then(void 0, function(err) {
259
+ dirty = true;
260
+ throw err;
261
+ });
262
+ }
263
+ function flushNow() {
264
+ if (flushTimer !== null) {
265
+ clearTimeout(flushTimer);
266
+ flushTimer = null;
267
+ }
268
+ dirty = false;
269
+ const next = inFlight.then(persist, persist);
270
+ inFlight = settled(next);
271
+ return next;
272
+ }
273
+ function scheduleFlush() {
274
+ dirty = true;
275
+ if (flushTimer !== null) return;
276
+ flushTimer = setTimeout(function() {
277
+ flushTimer = null;
278
+ void settled(flushNow());
279
+ }, FLUSH_DELAY_MS);
280
+ }
281
+ function whenHydrated() {
282
+ return hydration ? hydration : Promise.resolve(mode);
283
+ }
284
+ function attachUnloadFlush() {
285
+ const flushIfDirty = function() {
286
+ if (dirty) void settled(flushNow());
287
+ };
288
+ try {
289
+ window.addEventListener("pagehide", flushIfDirty);
290
+ window.addEventListener("visibilitychange", function() {
291
+ if (document.visibilityState === "hidden") flushIfDirty();
292
+ });
293
+ } catch (err) {
294
+ }
295
+ }
296
+ function install() {
297
+ if (shim) return mode;
298
+ installSessionStorageShim();
299
+ if (nativeStorageWorks("localStorage")) {
300
+ mode = "native";
301
+ return mode;
302
+ }
303
+ const handle = createStorageShim({ onChange: scheduleFlush, wouldOverflow });
304
+ try {
305
+ Object.defineProperty(window, "localStorage", {
306
+ value: handle.storage,
307
+ configurable: true
308
+ });
309
+ } catch (err) {
310
+ return mode;
311
+ }
312
+ shim = handle;
313
+ mode = "cloud";
314
+ attachUnloadFlush();
315
+ void hydrateFromCloud();
316
+ return mode;
317
+ }
318
+ return {
319
+ install,
320
+ active: function() {
321
+ return shim !== null;
322
+ },
323
+ ready: function() {
324
+ return whenHydrated();
325
+ },
326
+ flush: function() {
327
+ if (!shim && appBlob === null) return Promise.resolve();
328
+ return flushNow();
329
+ },
330
+ writeApp: function(blob) {
331
+ if (options.byteLength(buildEnvelope(shim ? shim.entries() : {}, blob)) > options.maxBytes) {
332
+ return Promise.reject(storageError("too_large"));
333
+ }
334
+ appBlob = blob;
335
+ return flushNow();
336
+ },
337
+ get mode() {
338
+ return mode;
339
+ }
340
+ };
341
+ }
342
+
33
343
  // src/core.ts
34
344
  var BOUNTY_BOARD_URL = "https://www.bountyboard.gg/arcade";
35
345
  var ADSENSE_CLIENT_ID = "ca-pub-7727110228190547";
@@ -818,6 +1128,16 @@ function createBBArcade() {
818
1128
  if (!hostnameAllowed(event.origin, DEFAULT_ALLOW)) return;
819
1129
  applyHostConfig(event.data);
820
1130
  });
1131
+ const storage = createStorageController({
1132
+ save: function(blob) {
1133
+ return request("save", blob);
1134
+ },
1135
+ load: function() {
1136
+ return request("load");
1137
+ },
1138
+ byteLength: utf8ByteLength,
1139
+ maxBytes: MAX_SAVE_BYTES
1140
+ });
821
1141
  const sdk = {
822
1142
  lockToHost,
823
1143
  init,
@@ -854,11 +1174,15 @@ function createBBArcade() {
854
1174
  if (utf8ByteLength(data) > MAX_SAVE_BYTES) {
855
1175
  return Promise.reject(sdkError("too_large"));
856
1176
  }
1177
+ if (storage.active()) return storage.writeApp(data);
857
1178
  return request("save", data);
858
1179
  },
859
1180
  load: function() {
860
- return request("load");
1181
+ return request("load").then(function(raw) {
1182
+ return parseEnvelope(raw).app;
1183
+ });
861
1184
  },
1185
+ storage,
862
1186
  getPlayer,
863
1187
  onPlayerChange: function(handler) {
864
1188
  if (typeof handler !== "function") return function() {
@@ -934,6 +1258,14 @@ function createInertBBArcade() {
934
1258
  rewardedBreak: () => Promise.resolve(false),
935
1259
  save: () => Promise.reject(sdkError2("unsupported")),
936
1260
  load: () => Promise.reject(sdkError2("unsupported")),
1261
+ // No window to shim and nothing to persist to: report the honest mode
1262
+ // rather than pretending an install succeeded.
1263
+ storage: {
1264
+ install: () => "memory",
1265
+ ready: () => Promise.resolve("memory"),
1266
+ flush: () => Promise.resolve(),
1267
+ mode: "memory"
1268
+ },
937
1269
  getPlayer: () => Promise.resolve(null),
938
1270
  onPlayerChange: () => () => {
939
1271
  },
@@ -1045,7 +1377,7 @@ var RoomConnection = class {
1045
1377
  /** Connect and resolve on the server's welcome (post-ticket-verification). */
1046
1378
  connect(grant) {
1047
1379
  return new Promise((resolve, reject) => {
1048
- let settled = false;
1380
+ let settled2 = false;
1049
1381
  const url = grant.roomUrl + (grant.roomUrl.indexOf("?") === -1 ? "?" : "&") + "ticket=" + encodeURIComponent(grant.ticket) + (this.joinData === null ? "" : "&join=" + encodeURIComponent(this.joinData));
1050
1382
  const connectStartedAt = Date.now();
1051
1383
  let ws;
@@ -1056,8 +1388,8 @@ var RoomConnection = class {
1056
1388
  }
1057
1389
  this.ws = ws;
1058
1390
  const welcomeTimer = setTimeout(() => {
1059
- if (settled) return;
1060
- settled = true;
1391
+ if (settled2) return;
1392
+ settled2 = true;
1061
1393
  this.pendingConnect = null;
1062
1394
  this.terminal = true;
1063
1395
  try {
@@ -1091,8 +1423,8 @@ var RoomConnection = class {
1091
1423
  this.connected = true;
1092
1424
  this.reconnectAttempts = 0;
1093
1425
  this.latencyMs = Math.max(0, Date.now() - connectStartedAt);
1094
- if (!settled) {
1095
- settled = true;
1426
+ if (!settled2) {
1427
+ settled2 = true;
1096
1428
  this.pendingConnect = null;
1097
1429
  clearTimeout(welcomeTimer);
1098
1430
  resolve(this);
@@ -1132,8 +1464,8 @@ var RoomConnection = class {
1132
1464
  }
1133
1465
  if (msg.t === "error") {
1134
1466
  const code = String((_e = msg.code) != null ? _e : "error");
1135
- if (!settled) {
1136
- settled = true;
1467
+ if (!settled2) {
1468
+ settled2 = true;
1137
1469
  this.terminal = true;
1138
1470
  this.pendingConnect = null;
1139
1471
  clearTimeout(welcomeTimer);
@@ -1150,8 +1482,8 @@ var RoomConnection = class {
1150
1482
  ws.onclose = (event) => {
1151
1483
  this.connected = false;
1152
1484
  if (event && event.code === SEAT_FULL_CLOSE_CODE) this.terminal = true;
1153
- if (!settled) {
1154
- settled = true;
1485
+ if (!settled2) {
1486
+ settled2 = true;
1155
1487
  this.pendingConnect = null;
1156
1488
  clearTimeout(welcomeTimer);
1157
1489
  reject(sdkError3("error"));