@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.
- package/AGENTS.md +12 -3
- package/CHANGELOG.md +35 -0
- package/README.md +50 -3
- 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/llms.txt +137 -52
- package/package.json +1 -1
- package/dist/chunk-KDBBR532.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -19,15 +19,24 @@ https://www.bountyboard.gg/arcade/sdk/llms.txt — this file is about using it C
|
|
|
19
19
|
the pattern for new integrations.
|
|
20
20
|
3. **Scores are integers and `gameOver` fires exactly once per run.** Don't submit
|
|
21
21
|
post-game-over corrections; the server enforces per-game plausibility caps and rejects
|
|
22
|
-
implausible values. Call `submitScore` freely during play — the host throttles.
|
|
22
|
+
implausible values. Call `submitScore` freely during play — the host throttles. Wiring the
|
|
23
|
+
calls is only half of a leaderboard: the studio must also declare Leaderboards on the game
|
|
24
|
+
in its Arcade submission, or the host rejects every score and the game never learns why.
|
|
23
25
|
4. **Always handle `getPlayer() === null`** (guests, standalone, off-host). The SDK never
|
|
24
26
|
provides ids, emails, or roles — do not design features that need them.
|
|
25
27
|
5. **One save blob.** Serialize the whole progress object into a single `save(string)` (~1MB
|
|
26
28
|
cap). Save on checkpoints/game-over, never per frame. Handle all five rejection codes; keep
|
|
27
29
|
localStorage as the standalone fallback (hosted builds have no localStorage — the sandbox is
|
|
28
|
-
opaque-origin — so the SDK save IS
|
|
30
|
+
opaque-origin, where reading it THROWS rather than returning empty — so the SDK save IS
|
|
31
|
+
primary there). **Exception: engine exports.** When the game is a GameMaker/Godot/Unity/
|
|
32
|
+
Construct build whose storage layer can't be rewired without patching engine internals, use
|
|
33
|
+
`storage.install()` instead: it swaps the throwing `localStorage` for a cloud-backed shim. The
|
|
34
|
+
script tag installs it for you; module consumers call it before boot. `getItem` is synchronous
|
|
35
|
+
and the cloud read is not, so anything reading progress at boot must await `storage.ready()`.
|
|
29
36
|
6. **`lockToHost()` before boot** when the studio wants anti-theft, with their own domains in
|
|
30
|
-
`allow`. It's a deterrent, not DRM — never present it as more.
|
|
37
|
+
`allow`. It's a deterrent, not DRM — never present it as more. Bounty Board's own hosts and
|
|
38
|
+
localhost are always allowed, so a build Bounty Board hosts passes no `allow` entries; never
|
|
39
|
+
ship the `play.yourgame.com` placeholder from the docs as if it were a real value.
|
|
31
40
|
7. **Multiplayer: clients send inputs, never state.** The approved room authority (a Bounty Board
|
|
32
41
|
module or a registered external server) simulates and validates; design the game so all
|
|
33
42
|
authority lives in that one server. Don't trust or display any value another client sent
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# @bountyboard/arcade-sdk
|
|
2
2
|
|
|
3
|
+
## 1.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0f53ae3: Add `BBArcade.storage`, a localStorage compatibility shim for engine exports.
|
|
8
|
+
|
|
9
|
+
Bounty-Board-hosted builds run on an opaque origin where `localStorage` throws
|
|
10
|
+
rather than returning empty. Games that own their source can just call
|
|
11
|
+
`save()`/`load()`, but engine runtimes bake synchronous storage in: GameMaker
|
|
12
|
+
HTML5 routes `ini_open`/`ini_write_*`/`game_save` through it, as do Godot,
|
|
13
|
+
Unity, and Construct.
|
|
14
|
+
|
|
15
|
+
`storage.install()` swaps the throwing `localStorage` for a Storage-shaped
|
|
16
|
+
object backed by the SDK's cloud save, so those builds persist per player and
|
|
17
|
+
across devices with no engine changes. It is idempotent and a no-op wherever a
|
|
18
|
+
real Storage works, so standalone play and URL embeds are untouched. The
|
|
19
|
+
script-tag build installs it at load; module consumers keep a side-effect-free
|
|
20
|
+
import and call it themselves before boot.
|
|
21
|
+
|
|
22
|
+
`ready()` resolves once the cloud read lands (`getItem` is synchronous, the read
|
|
23
|
+
is not), `flush()` forces the debounced write out, and `mode` reports
|
|
24
|
+
`'native' | 'cloud' | 'memory'`. `sessionStorage` is shimmed memory-only. With
|
|
25
|
+
the shim active `save()`/`load()` share the player's one save slot through a
|
|
26
|
+
tagged envelope that `load()` unwraps; a raw blob written before the shim
|
|
27
|
+
existed reads back untouched.
|
|
28
|
+
|
|
29
|
+
The shim never writes over a save it has not successfully read. A read that
|
|
30
|
+
FAILS settles just like one that succeeds, so persisting on that basis would
|
|
31
|
+
serialize only the current session's keys and wipe the rest; instead a failed
|
|
32
|
+
read is retried on the next write, and a write that still cannot confirm what
|
|
33
|
+
the player had is refused rather than allowed to overwrite it blind. `save()`
|
|
34
|
+
is size-checked against the whole envelope before it commits, so a rejected
|
|
35
|
+
oversized blob cannot leave the session unable to write at all. Keys named
|
|
36
|
+
`__proto__` are stored and restored like any other, matching a real `Storage`.
|
|
37
|
+
|
|
3
38
|
## 1.2.0
|
|
4
39
|
|
|
5
40
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -18,7 +18,10 @@ npm install @bountyboard/arcade-sdk
|
|
|
18
18
|
```ts
|
|
19
19
|
import { BBArcade } from '@bountyboard/arcade-sdk';
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
// Optional; call before boot. 'play.yourgame.com' is a placeholder: allow lists
|
|
22
|
+
// the domains YOU host on, and Bounty Board plus localhost are always allowed,
|
|
23
|
+
// so a Bounty-hosted build needs no allow list (or no call at all).
|
|
24
|
+
BBArcade.lockToHost({ allow: ['play.yourgame.com'] });
|
|
22
25
|
void BBArcade.init(); // host handshake; awaiting is optional
|
|
23
26
|
|
|
24
27
|
export function onGameReady() {
|
|
@@ -65,7 +68,7 @@ specifically needs the global build, import `@bountyboard/arcade-sdk/global`.
|
|
|
65
68
|
Bundle note: importing the package attaches the host `postMessage` listeners
|
|
66
69
|
immediately (the host pushes its config at load time, so listener timing is
|
|
67
70
|
part of the protocol), so the package is honestly marked `sideEffects: true`
|
|
68
|
-
and will not tree-shake. The full script-tag build is ~
|
|
71
|
+
and will not tree-shake. The full script-tag build is ~55 KB unminified;
|
|
69
72
|
module consumers who only use multiplayer still get the whole core.
|
|
70
73
|
|
|
71
74
|
## Runtime support
|
|
@@ -79,6 +82,7 @@ the game runs determines which host-backed features are available.
|
|
|
79
82
|
| Player identity | Player or `null` | Player or `null` | `null` |
|
|
80
83
|
| A/B variants | Stable assignment | Stable assignment | Alphabetical control |
|
|
81
84
|
| Cloud save / load | Logged-in players | Unsupported | Unsupported |
|
|
85
|
+
| Native `localStorage` | Throws (opaque) | Works on your origin | Works |
|
|
82
86
|
| Rewarded ads | Approved games | Unavailable (no payable attribution) | Unavailable |
|
|
83
87
|
| Multiplayer | Enabled authority | Enabled authority | Unsupported |
|
|
84
88
|
|
|
@@ -148,6 +152,47 @@ save/load rejection is a real `Error` with a typed `code`:
|
|
|
148
152
|
unsupported | unauthenticated | too_large | rejected | error
|
|
149
153
|
```
|
|
150
154
|
|
|
155
|
+
## Engine exports: the localStorage shim
|
|
156
|
+
|
|
157
|
+
Prefer `save()`/`load()` when you control the source. The shim is for engine
|
|
158
|
+
runtimes whose storage layer can't be rewired without patching engine
|
|
159
|
+
internals — GameMaker HTML5 (`ini_open`, `ini_write_*`, and `game_save` all sit
|
|
160
|
+
on `localStorage`), Godot, Unity, Construct. It replaces the throwing
|
|
161
|
+
`localStorage` with a Storage-shaped object backed by your cloud save, so those
|
|
162
|
+
builds persist per player and across devices with no engine changes.
|
|
163
|
+
|
|
164
|
+
The script-tag build installs it at load, so the only requirement is load order:
|
|
165
|
+
|
|
166
|
+
```html
|
|
167
|
+
<script src="https://www.bountyboard.gg/arcade-sdk/v1.js"></script>
|
|
168
|
+
<script src="html5game/YourGame.js"></script>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Module consumers keep a side-effect-free import and install it themselves,
|
|
172
|
+
before any engine code runs:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
BBArcade.storage.install(); // idempotent; no-op where a real Storage works
|
|
176
|
+
|
|
177
|
+
// getItem() is synchronous but the cloud read that fills it is not.
|
|
178
|
+
const mode = await BBArcade.storage.ready(); // 'cloud' | 'memory' | 'native'
|
|
179
|
+
startGame();
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Writes made before hydration are kept and merged with the cloud read (your
|
|
183
|
+
write wins; a `removeItem` isn't resurrected), and nothing is pushed to the
|
|
184
|
+
server until that read has SUCCEEDED — a failed read is retried, and a write
|
|
185
|
+
that still can't confirm what the player had is refused rather than allowed to
|
|
186
|
+
overwrite it blind, so neither a fresh-start write at boot nor a transport blip
|
|
187
|
+
can wipe an existing save. `sessionStorage` is shimmed too, but memory-only. Guests and
|
|
188
|
+
standalone play settle in `memory` mode: storage works for the session and is
|
|
189
|
+
never persisted. `setItem` throws `QuotaExceededError` past the save cap, like
|
|
190
|
+
a real Storage.
|
|
191
|
+
|
|
192
|
+
`save()`/`load()` keep working alongside it: the player has one save slot, so
|
|
193
|
+
both halves share it inside a tagged envelope that `load()` unwraps. A raw blob
|
|
194
|
+
written before the shim existed reads back untouched.
|
|
195
|
+
|
|
151
196
|
## Player identity and A/B variants
|
|
152
197
|
|
|
153
198
|
The SDK only provides public display identity. It never exposes account ids,
|
|
@@ -325,6 +370,8 @@ before adapting an existing server.
|
|
|
325
370
|
// Anti-rehosting deterrent, not DRM. Call before boot. When the embedder
|
|
326
371
|
// can't be read from browser signals (opaque-origin sandbox), the SDK asks
|
|
327
372
|
// the embedding page and blocks if nothing answers within ~5 seconds.
|
|
373
|
+
// The allowed domains below are placeholders for your own; Bounty Board and
|
|
374
|
+
// localhost are allowed by default, so a Bounty-hosted build passes none.
|
|
328
375
|
BBArcade.lockToHost({
|
|
329
376
|
allow: ['play.yourgame.com', 'preview.yourgame.com'],
|
|
330
377
|
signed: true,
|
|
@@ -357,7 +404,7 @@ Give coding agents the public
|
|
|
357
404
|
| ------------- | ------------------------------------------------------------------------------------- |
|
|
358
405
|
| Lifecycle | `init`, `configure`, `ready` / `gameLoadingFinished`, `gameplayStart`, `gameplayStop` |
|
|
359
406
|
| Scores | `submitScore`, `gameOver` |
|
|
360
|
-
| Player data | `save`, `load`, `getPlayer`, `onPlayerChange`, `getVariant`
|
|
407
|
+
| Player data | `save`, `load`, `storage.install` / `ready` / `flush`, `getPlayer`, `onPlayerChange`, `getVariant` |
|
|
361
408
|
| Rewarded ads | `prepareRewardedAd`, `rewardedAd`, `rewardedBreak`, `preloadRewardedAds` |
|
|
362
409
|
| Security / XR | `lockToHost`, `xrSessionStart`, `xrSessionEnd` |
|
|
363
410
|
| Multiplayer | `joinRoom`, `room.trySend`, `room.on`, `room.leave`, `room.latencyMs` |
|
|
@@ -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";
|
|
@@ -791,6 +1101,16 @@ function createBBArcade() {
|
|
|
791
1101
|
if (!hostnameAllowed(event.origin, DEFAULT_ALLOW)) return;
|
|
792
1102
|
applyHostConfig(event.data);
|
|
793
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
|
+
});
|
|
794
1114
|
const sdk = {
|
|
795
1115
|
lockToHost,
|
|
796
1116
|
init,
|
|
@@ -827,11 +1147,15 @@ function createBBArcade() {
|
|
|
827
1147
|
if (utf8ByteLength(data) > MAX_SAVE_BYTES) {
|
|
828
1148
|
return Promise.reject(sdkError("too_large"));
|
|
829
1149
|
}
|
|
1150
|
+
if (storage.active()) return storage.writeApp(data);
|
|
830
1151
|
return request("save", data);
|
|
831
1152
|
},
|
|
832
1153
|
load: function() {
|
|
833
|
-
return request("load")
|
|
1154
|
+
return request("load").then(function(raw) {
|
|
1155
|
+
return parseEnvelope(raw).app;
|
|
1156
|
+
});
|
|
834
1157
|
},
|
|
1158
|
+
storage,
|
|
835
1159
|
getPlayer,
|
|
836
1160
|
onPlayerChange: function(handler) {
|
|
837
1161
|
if (typeof handler !== "function") return function() {
|
|
@@ -907,6 +1231,14 @@ function createInertBBArcade() {
|
|
|
907
1231
|
rewardedBreak: () => Promise.resolve(false),
|
|
908
1232
|
save: () => Promise.reject(sdkError2("unsupported")),
|
|
909
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
|
+
},
|
|
910
1242
|
getPlayer: () => Promise.resolve(null),
|
|
911
1243
|
onPlayerChange: () => () => {
|
|
912
1244
|
},
|
|
@@ -927,4 +1259,4 @@ export {
|
|
|
927
1259
|
BBArcade,
|
|
928
1260
|
src_default
|
|
929
1261
|
};
|
|
930
|
-
//# sourceMappingURL=chunk-
|
|
1262
|
+
//# sourceMappingURL=chunk-PYW5F45R.js.map
|