@bountyboard/arcade-sdk 1.2.0 → 1.4.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";
@@ -819,6 +1129,16 @@ function createBBArcade() {
819
1129
  if (!hostnameAllowed(event.origin, DEFAULT_ALLOW)) return;
820
1130
  applyHostConfig(event.data);
821
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
+ });
822
1142
  const sdk = {
823
1143
  lockToHost,
824
1144
  init,
@@ -855,11 +1175,15 @@ function createBBArcade() {
855
1175
  if (utf8ByteLength(data) > MAX_SAVE_BYTES) {
856
1176
  return Promise.reject(sdkError("too_large"));
857
1177
  }
1178
+ if (storage.active()) return storage.writeApp(data);
858
1179
  return request("save", data);
859
1180
  },
860
1181
  load: function() {
861
- return request("load");
1182
+ return request("load").then(function(raw) {
1183
+ return parseEnvelope(raw).app;
1184
+ });
862
1185
  },
1186
+ storage,
863
1187
  getPlayer,
864
1188
  onPlayerChange: function(handler) {
865
1189
  if (typeof handler !== "function") return function() {
@@ -935,6 +1259,14 @@ function createInertBBArcade() {
935
1259
  rewardedBreak: () => Promise.resolve(false),
936
1260
  save: () => Promise.reject(sdkError2("unsupported")),
937
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
+ },
938
1270
  getPlayer: () => Promise.resolve(null),
939
1271
  onPlayerChange: () => () => {
940
1272
  },