@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/global.js
CHANGED
|
@@ -5,6 +5,316 @@
|
|
|
5
5
|
var SDK_MESSAGE_SOURCE = "bb-arcade";
|
|
6
6
|
var HOST_MESSAGE_SOURCE = "bb-arcade-host";
|
|
7
7
|
|
|
8
|
+
// src/storage.ts
|
|
9
|
+
var FLUSH_DELAY_MS = 400;
|
|
10
|
+
var ENVELOPE_TAG = "__bbArcadeStorage";
|
|
11
|
+
var ENVELOPE_VERSION = 1;
|
|
12
|
+
function hasOwn(target, key) {
|
|
13
|
+
return Object.prototype.hasOwnProperty.call(target, key);
|
|
14
|
+
}
|
|
15
|
+
function emptyMap() {
|
|
16
|
+
return /* @__PURE__ */ Object.create(null);
|
|
17
|
+
}
|
|
18
|
+
function storageError(code) {
|
|
19
|
+
const err = new Error(code);
|
|
20
|
+
err.code = code;
|
|
21
|
+
return err;
|
|
22
|
+
}
|
|
23
|
+
function parseEnvelope(raw) {
|
|
24
|
+
if (raw == null) return { ls: {}, app: null };
|
|
25
|
+
if (raw.charAt(0) !== "{") return { ls: {}, app: raw };
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = JSON.parse(raw);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
return { ls: {}, app: raw };
|
|
31
|
+
}
|
|
32
|
+
if (!parsed || typeof parsed !== "object") return { ls: {}, app: raw };
|
|
33
|
+
const record = parsed;
|
|
34
|
+
if (record[ENVELOPE_TAG] !== ENVELOPE_VERSION) return { ls: {}, app: raw };
|
|
35
|
+
const ls = emptyMap();
|
|
36
|
+
const source = record.ls;
|
|
37
|
+
if (source && typeof source === "object") {
|
|
38
|
+
const entries = source;
|
|
39
|
+
for (const key in entries) {
|
|
40
|
+
if (hasOwn(entries, key) && typeof entries[key] === "string") {
|
|
41
|
+
ls[key] = entries[key];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { ls, app: typeof record.app === "string" ? record.app : null };
|
|
46
|
+
}
|
|
47
|
+
function buildEnvelope(ls, app) {
|
|
48
|
+
const envelope = {};
|
|
49
|
+
envelope[ENVELOPE_TAG] = ENVELOPE_VERSION;
|
|
50
|
+
envelope.ls = ls;
|
|
51
|
+
envelope.app = app;
|
|
52
|
+
return JSON.stringify(envelope);
|
|
53
|
+
}
|
|
54
|
+
function quotaError() {
|
|
55
|
+
try {
|
|
56
|
+
return new DOMException("Cloud save quota exceeded", "QuotaExceededError");
|
|
57
|
+
} catch (err) {
|
|
58
|
+
const fallback = new Error("Cloud save quota exceeded");
|
|
59
|
+
fallback.name = "QuotaExceededError";
|
|
60
|
+
return fallback;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
var RESERVED = {
|
|
64
|
+
getItem: true,
|
|
65
|
+
setItem: true,
|
|
66
|
+
removeItem: true,
|
|
67
|
+
clear: true,
|
|
68
|
+
key: true,
|
|
69
|
+
length: true
|
|
70
|
+
};
|
|
71
|
+
function createStorageShim(backend = {}) {
|
|
72
|
+
const map = emptyMap();
|
|
73
|
+
const localWrites = emptyMap();
|
|
74
|
+
function keys() {
|
|
75
|
+
return Object.keys(map);
|
|
76
|
+
}
|
|
77
|
+
function changed() {
|
|
78
|
+
if (backend.onChange) backend.onChange();
|
|
79
|
+
}
|
|
80
|
+
const api = {
|
|
81
|
+
getItem: function(key) {
|
|
82
|
+
const name = String(key);
|
|
83
|
+
return hasOwn(map, name) ? map[name] : null;
|
|
84
|
+
},
|
|
85
|
+
setItem: function(key, value) {
|
|
86
|
+
const name = String(key);
|
|
87
|
+
const next = String(value);
|
|
88
|
+
if (backend.wouldOverflow && backend.wouldOverflow(name, next)) throw quotaError();
|
|
89
|
+
map[name] = next;
|
|
90
|
+
localWrites[name] = true;
|
|
91
|
+
changed();
|
|
92
|
+
},
|
|
93
|
+
removeItem: function(key) {
|
|
94
|
+
const name = String(key);
|
|
95
|
+
localWrites[name] = true;
|
|
96
|
+
if (!hasOwn(map, name)) return;
|
|
97
|
+
delete map[name];
|
|
98
|
+
changed();
|
|
99
|
+
},
|
|
100
|
+
clear: function() {
|
|
101
|
+
const existing = keys();
|
|
102
|
+
for (let i = 0; i < existing.length; i++) {
|
|
103
|
+
delete map[existing[i]];
|
|
104
|
+
localWrites[existing[i]] = true;
|
|
105
|
+
}
|
|
106
|
+
changed();
|
|
107
|
+
},
|
|
108
|
+
key: function(index) {
|
|
109
|
+
const existing = keys();
|
|
110
|
+
const at = Number(index);
|
|
111
|
+
return at >= 0 && at < existing.length ? existing[at] : null;
|
|
112
|
+
},
|
|
113
|
+
get length() {
|
|
114
|
+
return keys().length;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const storage = new Proxy(api, {
|
|
118
|
+
get(target, prop, receiver) {
|
|
119
|
+
if (typeof prop !== "string") return Reflect.get(target, prop, receiver);
|
|
120
|
+
if (hasOwn(RESERVED, prop)) return Reflect.get(target, prop, receiver);
|
|
121
|
+
return hasOwn(map, prop) ? map[prop] : void 0;
|
|
122
|
+
},
|
|
123
|
+
set(target, prop, value) {
|
|
124
|
+
if (typeof prop !== "string" || hasOwn(RESERVED, prop)) return true;
|
|
125
|
+
api.setItem(prop, value);
|
|
126
|
+
return true;
|
|
127
|
+
},
|
|
128
|
+
has(target, prop) {
|
|
129
|
+
if (typeof prop !== "string") return Reflect.has(target, prop);
|
|
130
|
+
return hasOwn(RESERVED, prop) || hasOwn(map, prop);
|
|
131
|
+
},
|
|
132
|
+
deleteProperty(target, prop) {
|
|
133
|
+
if (typeof prop === "string" && !hasOwn(RESERVED, prop)) api.removeItem(prop);
|
|
134
|
+
return true;
|
|
135
|
+
},
|
|
136
|
+
ownKeys() {
|
|
137
|
+
return keys();
|
|
138
|
+
},
|
|
139
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
140
|
+
if (typeof prop !== "string" || !hasOwn(map, prop)) return void 0;
|
|
141
|
+
return { value: map[prop], writable: true, enumerable: true, configurable: true };
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
storage,
|
|
146
|
+
entries: function() {
|
|
147
|
+
const copy = emptyMap();
|
|
148
|
+
const existing = keys();
|
|
149
|
+
for (let i = 0; i < existing.length; i++) copy[existing[i]] = map[existing[i]];
|
|
150
|
+
return copy;
|
|
151
|
+
},
|
|
152
|
+
hydrate: function(values) {
|
|
153
|
+
for (const key in values) {
|
|
154
|
+
if (hasOwn(values, key) && !hasOwn(localWrites, key)) map[key] = values[key];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function settled(promise) {
|
|
160
|
+
return promise.then(
|
|
161
|
+
function() {
|
|
162
|
+
},
|
|
163
|
+
function() {
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
function installSessionStorageShim() {
|
|
168
|
+
if (nativeStorageWorks("sessionStorage")) return;
|
|
169
|
+
try {
|
|
170
|
+
Object.defineProperty(window, "sessionStorage", {
|
|
171
|
+
value: createStorageShim().storage,
|
|
172
|
+
configurable: true
|
|
173
|
+
});
|
|
174
|
+
} catch (err) {
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function nativeStorageWorks(area) {
|
|
178
|
+
try {
|
|
179
|
+
const store = window[area];
|
|
180
|
+
if (!store) return false;
|
|
181
|
+
const probe = "__bb_arcade_probe__";
|
|
182
|
+
store.setItem(probe, "1");
|
|
183
|
+
store.removeItem(probe);
|
|
184
|
+
return true;
|
|
185
|
+
} catch (err) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function createStorageController(options) {
|
|
190
|
+
let shim = null;
|
|
191
|
+
let mode = "native";
|
|
192
|
+
let appBlob = null;
|
|
193
|
+
let hydration = null;
|
|
194
|
+
let flushTimer = null;
|
|
195
|
+
let inFlight = Promise.resolve();
|
|
196
|
+
let dirty = false;
|
|
197
|
+
let hydrated = false;
|
|
198
|
+
function serialize() {
|
|
199
|
+
return buildEnvelope(shim ? shim.entries() : {}, appBlob);
|
|
200
|
+
}
|
|
201
|
+
function hydrateFromCloud() {
|
|
202
|
+
hydration = options.load().then(
|
|
203
|
+
function(raw) {
|
|
204
|
+
const envelope = parseEnvelope(raw);
|
|
205
|
+
if (shim) shim.hydrate(envelope.ls);
|
|
206
|
+
if (appBlob === null) appBlob = envelope.app;
|
|
207
|
+
hydrated = true;
|
|
208
|
+
return mode;
|
|
209
|
+
},
|
|
210
|
+
function(err) {
|
|
211
|
+
const code = err && err.code;
|
|
212
|
+
if (code === "unsupported" || code === "unauthenticated") mode = "memory";
|
|
213
|
+
return mode;
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
return hydration;
|
|
217
|
+
}
|
|
218
|
+
function wouldOverflow(key, value) {
|
|
219
|
+
if (!shim) return false;
|
|
220
|
+
const next = shim.entries();
|
|
221
|
+
next[key] = value;
|
|
222
|
+
return options.byteLength(buildEnvelope(next, appBlob)) > options.maxBytes;
|
|
223
|
+
}
|
|
224
|
+
function persist() {
|
|
225
|
+
return whenHydrated().then(function() {
|
|
226
|
+
if (mode === "memory") return;
|
|
227
|
+
if (hydrated) return options.save(serialize());
|
|
228
|
+
return hydrateFromCloud().then(function() {
|
|
229
|
+
if (mode === "memory") return;
|
|
230
|
+
if (!hydrated) throw storageError("error");
|
|
231
|
+
return options.save(serialize());
|
|
232
|
+
});
|
|
233
|
+
}).then(void 0, function(err) {
|
|
234
|
+
dirty = true;
|
|
235
|
+
throw err;
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
function flushNow() {
|
|
239
|
+
if (flushTimer !== null) {
|
|
240
|
+
clearTimeout(flushTimer);
|
|
241
|
+
flushTimer = null;
|
|
242
|
+
}
|
|
243
|
+
dirty = false;
|
|
244
|
+
const next = inFlight.then(persist, persist);
|
|
245
|
+
inFlight = settled(next);
|
|
246
|
+
return next;
|
|
247
|
+
}
|
|
248
|
+
function scheduleFlush() {
|
|
249
|
+
dirty = true;
|
|
250
|
+
if (flushTimer !== null) return;
|
|
251
|
+
flushTimer = setTimeout(function() {
|
|
252
|
+
flushTimer = null;
|
|
253
|
+
void settled(flushNow());
|
|
254
|
+
}, FLUSH_DELAY_MS);
|
|
255
|
+
}
|
|
256
|
+
function whenHydrated() {
|
|
257
|
+
return hydration ? hydration : Promise.resolve(mode);
|
|
258
|
+
}
|
|
259
|
+
function attachUnloadFlush() {
|
|
260
|
+
const flushIfDirty = function() {
|
|
261
|
+
if (dirty) void settled(flushNow());
|
|
262
|
+
};
|
|
263
|
+
try {
|
|
264
|
+
window.addEventListener("pagehide", flushIfDirty);
|
|
265
|
+
window.addEventListener("visibilitychange", function() {
|
|
266
|
+
if (document.visibilityState === "hidden") flushIfDirty();
|
|
267
|
+
});
|
|
268
|
+
} catch (err) {
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function install() {
|
|
272
|
+
if (shim) return mode;
|
|
273
|
+
installSessionStorageShim();
|
|
274
|
+
if (nativeStorageWorks("localStorage")) {
|
|
275
|
+
mode = "native";
|
|
276
|
+
return mode;
|
|
277
|
+
}
|
|
278
|
+
const handle = createStorageShim({ onChange: scheduleFlush, wouldOverflow });
|
|
279
|
+
try {
|
|
280
|
+
Object.defineProperty(window, "localStorage", {
|
|
281
|
+
value: handle.storage,
|
|
282
|
+
configurable: true
|
|
283
|
+
});
|
|
284
|
+
} catch (err) {
|
|
285
|
+
return mode;
|
|
286
|
+
}
|
|
287
|
+
shim = handle;
|
|
288
|
+
mode = "cloud";
|
|
289
|
+
attachUnloadFlush();
|
|
290
|
+
void hydrateFromCloud();
|
|
291
|
+
return mode;
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
install,
|
|
295
|
+
active: function() {
|
|
296
|
+
return shim !== null;
|
|
297
|
+
},
|
|
298
|
+
ready: function() {
|
|
299
|
+
return whenHydrated();
|
|
300
|
+
},
|
|
301
|
+
flush: function() {
|
|
302
|
+
if (!shim && appBlob === null) return Promise.resolve();
|
|
303
|
+
return flushNow();
|
|
304
|
+
},
|
|
305
|
+
writeApp: function(blob) {
|
|
306
|
+
if (options.byteLength(buildEnvelope(shim ? shim.entries() : {}, blob)) > options.maxBytes) {
|
|
307
|
+
return Promise.reject(storageError("too_large"));
|
|
308
|
+
}
|
|
309
|
+
appBlob = blob;
|
|
310
|
+
return flushNow();
|
|
311
|
+
},
|
|
312
|
+
get mode() {
|
|
313
|
+
return mode;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
8
318
|
// src/core.ts
|
|
9
319
|
var BOUNTY_BOARD_URL = "https://www.bountyboard.gg/arcade";
|
|
10
320
|
var ADSENSE_CLIENT_ID = "ca-pub-7727110228190547";
|
|
@@ -793,6 +1103,16 @@
|
|
|
793
1103
|
if (!hostnameAllowed(event.origin, DEFAULT_ALLOW)) return;
|
|
794
1104
|
applyHostConfig(event.data);
|
|
795
1105
|
});
|
|
1106
|
+
const storage = createStorageController({
|
|
1107
|
+
save: function(blob) {
|
|
1108
|
+
return request("save", blob);
|
|
1109
|
+
},
|
|
1110
|
+
load: function() {
|
|
1111
|
+
return request("load");
|
|
1112
|
+
},
|
|
1113
|
+
byteLength: utf8ByteLength,
|
|
1114
|
+
maxBytes: MAX_SAVE_BYTES
|
|
1115
|
+
});
|
|
796
1116
|
const sdk = {
|
|
797
1117
|
lockToHost,
|
|
798
1118
|
init,
|
|
@@ -829,11 +1149,15 @@
|
|
|
829
1149
|
if (utf8ByteLength(data) > MAX_SAVE_BYTES) {
|
|
830
1150
|
return Promise.reject(sdkError("too_large"));
|
|
831
1151
|
}
|
|
1152
|
+
if (storage.active()) return storage.writeApp(data);
|
|
832
1153
|
return request("save", data);
|
|
833
1154
|
},
|
|
834
1155
|
load: function() {
|
|
835
|
-
return request("load")
|
|
1156
|
+
return request("load").then(function(raw) {
|
|
1157
|
+
return parseEnvelope(raw).app;
|
|
1158
|
+
});
|
|
836
1159
|
},
|
|
1160
|
+
storage,
|
|
837
1161
|
getPlayer,
|
|
838
1162
|
onPlayerChange: function(handler) {
|
|
839
1163
|
if (typeof handler !== "function") return function() {
|
|
@@ -909,6 +1233,14 @@
|
|
|
909
1233
|
rewardedBreak: () => Promise.resolve(false),
|
|
910
1234
|
save: () => Promise.reject(sdkError2("unsupported")),
|
|
911
1235
|
load: () => Promise.reject(sdkError2("unsupported")),
|
|
1236
|
+
// No window to shim and nothing to persist to: report the honest mode
|
|
1237
|
+
// rather than pretending an install succeeded.
|
|
1238
|
+
storage: {
|
|
1239
|
+
install: () => "memory",
|
|
1240
|
+
ready: () => Promise.resolve("memory"),
|
|
1241
|
+
flush: () => Promise.resolve(),
|
|
1242
|
+
mode: "memory"
|
|
1243
|
+
},
|
|
912
1244
|
getPlayer: () => Promise.resolve(null),
|
|
913
1245
|
onPlayerChange: () => () => {
|
|
914
1246
|
},
|
|
@@ -1020,7 +1352,7 @@
|
|
|
1020
1352
|
/** Connect and resolve on the server's welcome (post-ticket-verification). */
|
|
1021
1353
|
connect(grant) {
|
|
1022
1354
|
return new Promise((resolve, reject) => {
|
|
1023
|
-
let
|
|
1355
|
+
let settled2 = false;
|
|
1024
1356
|
const url = grant.roomUrl + (grant.roomUrl.indexOf("?") === -1 ? "?" : "&") + "ticket=" + encodeURIComponent(grant.ticket) + (this.joinData === null ? "" : "&join=" + encodeURIComponent(this.joinData));
|
|
1025
1357
|
const connectStartedAt = Date.now();
|
|
1026
1358
|
let ws;
|
|
@@ -1031,8 +1363,8 @@
|
|
|
1031
1363
|
}
|
|
1032
1364
|
this.ws = ws;
|
|
1033
1365
|
const welcomeTimer = setTimeout(() => {
|
|
1034
|
-
if (
|
|
1035
|
-
|
|
1366
|
+
if (settled2) return;
|
|
1367
|
+
settled2 = true;
|
|
1036
1368
|
this.pendingConnect = null;
|
|
1037
1369
|
this.terminal = true;
|
|
1038
1370
|
try {
|
|
@@ -1066,8 +1398,8 @@
|
|
|
1066
1398
|
this.connected = true;
|
|
1067
1399
|
this.reconnectAttempts = 0;
|
|
1068
1400
|
this.latencyMs = Math.max(0, Date.now() - connectStartedAt);
|
|
1069
|
-
if (!
|
|
1070
|
-
|
|
1401
|
+
if (!settled2) {
|
|
1402
|
+
settled2 = true;
|
|
1071
1403
|
this.pendingConnect = null;
|
|
1072
1404
|
clearTimeout(welcomeTimer);
|
|
1073
1405
|
resolve(this);
|
|
@@ -1107,8 +1439,8 @@
|
|
|
1107
1439
|
}
|
|
1108
1440
|
if (msg.t === "error") {
|
|
1109
1441
|
const code = String((_e = msg.code) != null ? _e : "error");
|
|
1110
|
-
if (!
|
|
1111
|
-
|
|
1442
|
+
if (!settled2) {
|
|
1443
|
+
settled2 = true;
|
|
1112
1444
|
this.terminal = true;
|
|
1113
1445
|
this.pendingConnect = null;
|
|
1114
1446
|
clearTimeout(welcomeTimer);
|
|
@@ -1125,8 +1457,8 @@
|
|
|
1125
1457
|
ws.onclose = (event) => {
|
|
1126
1458
|
this.connected = false;
|
|
1127
1459
|
if (event && event.code === SEAT_FULL_CLOSE_CODE) this.terminal = true;
|
|
1128
|
-
if (!
|
|
1129
|
-
|
|
1460
|
+
if (!settled2) {
|
|
1461
|
+
settled2 = true;
|
|
1130
1462
|
this.pendingConnect = null;
|
|
1131
1463
|
clearTimeout(welcomeTimer);
|
|
1132
1464
|
reject(sdkError3("error"));
|
|
@@ -1273,4 +1605,5 @@
|
|
|
1273
1605
|
// src/global.ts
|
|
1274
1606
|
BBArcade.multiplayer = multiplayer;
|
|
1275
1607
|
window.BBArcade = BBArcade;
|
|
1608
|
+
BBArcade.storage.install();
|
|
1276
1609
|
})();
|