castle-web-sdk 0.4.19 → 0.4.21
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/commands.d.ts +2 -0
- package/dist/storage.js +35 -5
- package/package.json +1 -1
package/dist/commands.d.ts
CHANGED
|
@@ -62,6 +62,7 @@ export interface CommandParams {
|
|
|
62
62
|
"deckStorage.load": Record<string, never>;
|
|
63
63
|
"deckStorage.update": {
|
|
64
64
|
updates: StorageUpdate[];
|
|
65
|
+
unload?: boolean;
|
|
65
66
|
};
|
|
66
67
|
"sharedDeckStorage.load": {
|
|
67
68
|
scope: SharedScope;
|
|
@@ -71,6 +72,7 @@ export interface CommandParams {
|
|
|
71
72
|
"sharedDeckStorage.update": {
|
|
72
73
|
scope: SharedScope;
|
|
73
74
|
updates: StorageUpdate[];
|
|
75
|
+
unload?: boolean;
|
|
74
76
|
};
|
|
75
77
|
"leaderboard.fetch": {
|
|
76
78
|
variable: string;
|
package/dist/storage.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CastleError } from "./errors";
|
|
2
2
|
import { hostRequest } from "./transport";
|
|
3
|
-
const FLUSH_INTERVAL_MS =
|
|
3
|
+
const FLUSH_INTERVAL_MS = 1000;
|
|
4
4
|
// Per-player private storage. The host stamps deckId/sessionId, so one deck
|
|
5
5
|
// session = one stable blob: load once, then serve reads from cache and batch
|
|
6
6
|
// writes on a 2s flush. Dirty writes overlay the server blob so an in-flight
|
|
@@ -38,12 +38,13 @@ class PrivateStorageImpl {
|
|
|
38
38
|
this.cache = decodeStorageBlob(blob, "Storage.get");
|
|
39
39
|
this.overlayDirty();
|
|
40
40
|
}
|
|
41
|
-
async flush() {
|
|
41
|
+
async flush(unload = false) {
|
|
42
42
|
if (this.dirty.size === 0)
|
|
43
43
|
return;
|
|
44
44
|
const snapshot = new Map(this.dirty);
|
|
45
45
|
const { blob } = await hostRequest("deckStorage.update", {
|
|
46
46
|
updates: updatesFromDirty(snapshot),
|
|
47
|
+
unload,
|
|
47
48
|
});
|
|
48
49
|
this.cache = decodeStorageBlob(blob, "Storage.flush");
|
|
49
50
|
clearAcknowledgedDirty(this.dirty, snapshot);
|
|
@@ -58,6 +59,7 @@ class PrivateStorageImpl {
|
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
scheduleFlush() {
|
|
62
|
+
hookUnloadFlush();
|
|
61
63
|
if (this.flushTimer)
|
|
62
64
|
return;
|
|
63
65
|
this.flushTimer = setTimeout(() => {
|
|
@@ -65,6 +67,9 @@ class PrivateStorageImpl {
|
|
|
65
67
|
void this.flush();
|
|
66
68
|
}, FLUSH_INTERVAL_MS);
|
|
67
69
|
}
|
|
70
|
+
flushNow() {
|
|
71
|
+
void this.flush(true);
|
|
72
|
+
}
|
|
68
73
|
}
|
|
69
74
|
// Cross-player storage. Writes always target the current player (the host
|
|
70
75
|
// forces 'user'-scope writes to whoever is signed in), so dirty writes bucket
|
|
@@ -151,18 +156,19 @@ class SharedStorageImpl {
|
|
|
151
156
|
read.resolve(value);
|
|
152
157
|
}
|
|
153
158
|
}
|
|
154
|
-
async flush() {
|
|
159
|
+
async flush(unload = false) {
|
|
155
160
|
if (this.dirtyBuckets.size === 0)
|
|
156
161
|
return;
|
|
157
|
-
const tasks = Array.from(this.dirtyBuckets, ([bucketKey, dirty]) => this.flushBucket(bucketKey, new Map(dirty)));
|
|
162
|
+
const tasks = Array.from(this.dirtyBuckets, ([bucketKey, dirty]) => this.flushBucket(bucketKey, new Map(dirty), unload));
|
|
158
163
|
await Promise.all(tasks);
|
|
159
164
|
}
|
|
160
|
-
async flushBucket(bucketKey, snapshot) {
|
|
165
|
+
async flushBucket(bucketKey, snapshot, unload = false) {
|
|
161
166
|
if (snapshot.size === 0)
|
|
162
167
|
return;
|
|
163
168
|
await hostRequest("sharedDeckStorage.update", {
|
|
164
169
|
scope: scopeFromWriteKey(bucketKey),
|
|
165
170
|
updates: updatesFromDirty(snapshot),
|
|
171
|
+
unload,
|
|
166
172
|
});
|
|
167
173
|
const dirty = this.dirtyBuckets.get(bucketKey);
|
|
168
174
|
if (!dirty)
|
|
@@ -172,6 +178,7 @@ class SharedStorageImpl {
|
|
|
172
178
|
this.dirtyBuckets.delete(bucketKey);
|
|
173
179
|
}
|
|
174
180
|
scheduleFlush() {
|
|
181
|
+
hookUnloadFlush();
|
|
175
182
|
if (this.flushTimer)
|
|
176
183
|
return;
|
|
177
184
|
this.flushTimer = setTimeout(() => {
|
|
@@ -179,6 +186,29 @@ class SharedStorageImpl {
|
|
|
179
186
|
void this.flush().catch(reportSharedStorageError);
|
|
180
187
|
}, FLUSH_INTERVAL_MS);
|
|
181
188
|
}
|
|
189
|
+
flushNow() {
|
|
190
|
+
void this.flush(true).catch(reportSharedStorageError);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Writes batch on a timer, so leaving the page inside that window would lose
|
|
194
|
+
// them -- a deck saving progress as someone quits is exactly when it matters.
|
|
195
|
+
// `pagehide` is the reliable signal on iOS, where unload often never fires, and
|
|
196
|
+
// visibilitychange covers backgrounding without a navigation. Hooked on the
|
|
197
|
+
// first write so a deck that never stores anything registers nothing.
|
|
198
|
+
let unloadFlushHooked = false;
|
|
199
|
+
function hookUnloadFlush() {
|
|
200
|
+
if (unloadFlushHooked || typeof window === "undefined")
|
|
201
|
+
return;
|
|
202
|
+
unloadFlushHooked = true;
|
|
203
|
+
const flush = () => {
|
|
204
|
+
Storage.flushNow();
|
|
205
|
+
SharedStorage.flushNow();
|
|
206
|
+
};
|
|
207
|
+
window.addEventListener("pagehide", flush);
|
|
208
|
+
document.addEventListener("visibilitychange", () => {
|
|
209
|
+
if (document.visibilityState === "hidden")
|
|
210
|
+
flush();
|
|
211
|
+
});
|
|
182
212
|
}
|
|
183
213
|
export const Storage = new PrivateStorageImpl();
|
|
184
214
|
export const SharedStorage = new SharedStorageImpl();
|