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.
@@ -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 = 2000;
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-sdk",
3
- "version": "0.4.19",
3
+ "version": "0.4.21",
4
4
  "type": "module",
5
5
  "main": "dist/castle.js",
6
6
  "types": "dist/castle.d.ts",