@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/AGENTS.md +12 -3
- package/CHANGELOG.md +48 -1
- package/README.md +56 -9
- package/dist/{chunk-KDBBR532.js → chunk-PYW5F45R.js} +334 -2
- package/dist/chunk-PYW5F45R.js.map +1 -0
- package/dist/global-sdk.d.ts +47 -0
- package/dist/global.js +343 -10
- package/dist/index.cjs +333 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/multiplayer.cjs +342 -10
- package/dist/multiplayer.cjs.map +1 -1
- package/dist/multiplayer.d.cts +2 -2
- package/dist/multiplayer.d.ts +2 -2
- package/dist/multiplayer.js +1 -1
- package/dist/{types-dtsNXYZW.d.cts → types-B5bqWnHH.d.cts} +46 -1
- package/dist/{types-dtsNXYZW.d.ts → types-B5bqWnHH.d.ts} +46 -1
- package/docs/external-authoritative-servers.md +21 -21
- package/docs/game-design-playbook.md +25 -25
- package/docs/llms.txt +257 -127
- package/docs/relay-rooms.md +22 -22
- package/package.json +2 -2
- package/dist/chunk-KDBBR532.js.map +0 -1
package/dist/multiplayer.cjs
CHANGED
|
@@ -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
|
|
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 (
|
|
1060
|
-
|
|
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 (!
|
|
1095
|
-
|
|
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 (!
|
|
1136
|
-
|
|
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 (!
|
|
1154
|
-
|
|
1485
|
+
if (!settled2) {
|
|
1486
|
+
settled2 = true;
|
|
1155
1487
|
this.pendingConnect = null;
|
|
1156
1488
|
clearTimeout(welcomeTimer);
|
|
1157
1489
|
reject(sdkError3("error"));
|