@stamprally/core 0.18.0 → 0.20.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/dist/index.js CHANGED
@@ -1198,8 +1198,6 @@ function errorValue(value, fallbackCode) {
1198
1198
  if (typeof value === "string") return { code: fallbackCode, message: value };
1199
1199
  return { code: fallbackCode, message: "Offline operation was rejected." };
1200
1200
  }
1201
- var syncLocks = /* @__PURE__ */ new Map();
1202
- var SYNC_LOCK_TTL_MS = 3e4;
1203
1201
  var DEFAULT_RETRY_OPTIONS = {
1204
1202
  maxRetries: 0,
1205
1203
  initialIntervalMs: 250,
@@ -1226,9 +1224,7 @@ var OfflineQueue = class {
1226
1224
  #synchronizeInstances;
1227
1225
  #retryOptions;
1228
1226
  #instanceId = randomId();
1229
- #lockStorage;
1230
- #observedLocks = /* @__PURE__ */ new Map();
1231
- #warnedMemoryLock = false;
1227
+ #warnedCapabilityMessages = /* @__PURE__ */ new Set();
1232
1228
  #capabilityWarningListener;
1233
1229
  #replayConfig;
1234
1230
  #storageListener;
@@ -1258,8 +1254,7 @@ var OfflineQueue = class {
1258
1254
  initialIntervalMs: Math.max(0, retryOptions.initialIntervalMs),
1259
1255
  backoffMultiplier: Math.max(1, retryOptions.backoffMultiplier)
1260
1256
  };
1261
- this.#lockStorage = options.storageLike ?? availableLocalStorage();
1262
- if (this.#synchronizeInstances) this.#subscribeToExternalChanges();
1257
+ if (this.#synchronizeInstances && this.#hasWebLocks()) this.#subscribeToExternalChanges();
1263
1258
  }
1264
1259
  get syncState() {
1265
1260
  return this.#state;
@@ -1268,12 +1263,14 @@ var OfflineQueue = class {
1268
1263
  return this.#operations.length;
1269
1264
  }
1270
1265
  get queueCapability() {
1271
- return this.#queueCapability;
1266
+ return {
1267
+ storage: this.#queueCapability,
1268
+ multiTabSync: this.#hasWebLocks() ? "supported_web_locks" : "disabled_unsafe_environment"
1269
+ };
1272
1270
  }
1273
1271
  get storageCapability() {
1274
1272
  if (this.#queueCapability === "memory") return "memory";
1275
- const locks = globalThis.navigator?.locks;
1276
- return locks !== void 0 || this.#lockStorage !== null ? this.#queueCapability : "volatile_single_tab";
1273
+ return this.#hasWebLocks() ? this.#queueCapability : "volatile_single_tab";
1277
1274
  }
1278
1275
  get isStoragePersistent() {
1279
1276
  return this.#queueCapability !== "memory";
@@ -1320,12 +1317,16 @@ var OfflineQueue = class {
1320
1317
  this.#queueCapability = "memory";
1321
1318
  this.#operations = [];
1322
1319
  this.#rejectedHistory = [];
1323
- this.#warnMemoryLock(
1320
+ this.#warnCapability(
1324
1321
  `Offline queue persistence is unavailable; queued data can be lost after reload (${String(error)}).`
1325
1322
  );
1326
1323
  }
1327
1324
  if (this.#queueCapability === "memory")
1328
- this.#warnMemoryLock("Offline queue persistence is unavailable; queued data is memory-only.");
1325
+ this.#warnCapability("Offline queue persistence is unavailable; queued data is memory-only.");
1326
+ if (this.queueCapability.multiTabSync === "disabled_unsafe_environment")
1327
+ this.#warnCapability(
1328
+ "Web Locks is unavailable; automatic cross-tab synchronization is disabled. Sync must be triggered by the foreground tab."
1329
+ );
1329
1330
  this.#loaded = true;
1330
1331
  }
1331
1332
  /** Releases browser listeners when the queue is no longer used. */
@@ -1336,7 +1337,6 @@ var OfflineQueue = class {
1336
1337
  this.#storageListener = void 0;
1337
1338
  this.#channel?.close();
1338
1339
  this.#channel = null;
1339
- this.#releaseSyncLock();
1340
1340
  }
1341
1341
  /** Selects a rally/user queue scope and loads its pending operations. */
1342
1342
  async setScope(rallyId, userId) {
@@ -1448,6 +1448,12 @@ var OfflineQueue = class {
1448
1448
  async retrySync(sender = this.#sender) {
1449
1449
  return this.sync(sender);
1450
1450
  }
1451
+ /** Applies a server-side batch response while preserving retryable operations. */
1452
+ async applySyncProgress(response) {
1453
+ await this.initialize();
1454
+ for (const result of response.results)
1455
+ await this.#applySyncOperationResult(result, response.currentState);
1456
+ }
1451
1457
  async #run(sender) {
1452
1458
  const locks = globalThis.navigator?.locks;
1453
1459
  if (locks !== void 0 && typeof locks.request === "function") {
@@ -1464,7 +1470,7 @@ var OfflineQueue = class {
1464
1470
  return false;
1465
1471
  }
1466
1472
  callbackStarted = true;
1467
- await this.#runWithStorageLock(sender);
1473
+ await this.#runSingleTab(sender);
1468
1474
  return true;
1469
1475
  }
1470
1476
  );
@@ -1474,22 +1480,12 @@ var OfflineQueue = class {
1474
1480
  if (callbackStarted) throw error;
1475
1481
  }
1476
1482
  }
1477
- if (this.storageCapability === "volatile_single_tab")
1478
- this.#warnMemoryLock(
1479
- "No cross-tab storage lock is available; offline sync is single-tab only."
1480
- );
1481
- await this.#runWithStorageLock(sender);
1483
+ await this.#runSingleTab(sender);
1482
1484
  }
1483
- async #runWithStorageLock(sender) {
1485
+ async #runSingleTab(sender) {
1484
1486
  this.#state = "syncing";
1485
1487
  this.#error = null;
1486
1488
  this.#changeListener?.();
1487
- if (!this.#acquireSyncLock()) {
1488
- await this.#reloadFromStorage();
1489
- this.#state = "idle";
1490
- this.#changeListener?.();
1491
- return;
1492
- }
1493
1489
  try {
1494
1490
  while (this.#operations.length > 0) {
1495
1491
  const operation = this.#operations[0];
@@ -1561,8 +1557,6 @@ var OfflineQueue = class {
1561
1557
  this.#error = cause instanceof Error ? cause : new Error(String(cause));
1562
1558
  this.#changeListener?.();
1563
1559
  throw this.#error;
1564
- } finally {
1565
- this.#releaseSyncLock();
1566
1560
  }
1567
1561
  }
1568
1562
  async #updateOperationStatus(status, attempts) {
@@ -1572,6 +1566,59 @@ var OfflineQueue = class {
1572
1566
  await this.#storage.save(this.#storageKey(), this.#operations);
1573
1567
  this.#announceChange();
1574
1568
  }
1569
+ async #applySyncOperationResult(result, currentState) {
1570
+ const operationIndex = this.#operations.findIndex(
1571
+ (operation2) => offlineOperationId(operation2) === result.operationId
1572
+ );
1573
+ const operation = operationIndex < 0 ? void 0 : this.#operations[operationIndex];
1574
+ if (operation === void 0) return;
1575
+ if (result.status === "FAILED_RETRYABLE") {
1576
+ const attempts = (operation.attempts ?? 0) + 1;
1577
+ this.#operations = [
1578
+ ...this.#operations.slice(0, operationIndex),
1579
+ { ...operation, status: "FAILED_RETRYABLE", attempts },
1580
+ ...this.#operations.slice(operationIndex + 1)
1581
+ ];
1582
+ await this.#storage.save(this.#storageKey(), this.#operations);
1583
+ this.#announceChange();
1584
+ await this.#syncResultListener?.({
1585
+ operation,
1586
+ status: "RETRYABLE_ERROR",
1587
+ error: { code: "FAILED_RETRYABLE", message: result.error }
1588
+ });
1589
+ return;
1590
+ }
1591
+ this.#operations = [
1592
+ ...this.#operations.slice(0, operationIndex),
1593
+ ...this.#operations.slice(operationIndex + 1)
1594
+ ];
1595
+ if (result.status === "REJECTED_PERMANENT") {
1596
+ const attempts = (operation.attempts ?? 0) + 1;
1597
+ const error = {
1598
+ code: result.errorCode,
1599
+ message: result.reason
1600
+ };
1601
+ this.#rejectedHistory = [
1602
+ ...this.#rejectedHistory,
1603
+ {
1604
+ operation: { ...operation, status: "REJECTED_PERMANENT", attempts },
1605
+ reason: error,
1606
+ errorCode: result.errorCode,
1607
+ rejectedAt: (/* @__PURE__ */ new Date()).toISOString(),
1608
+ attempts
1609
+ }
1610
+ ];
1611
+ await this.#saveRejectedHistory();
1612
+ }
1613
+ await this.#storage.save(this.#storageKey(), this.#operations);
1614
+ this.#announceChange();
1615
+ await this.#syncResultListener?.({
1616
+ operation,
1617
+ status: result.status,
1618
+ state: currentState,
1619
+ ...result.status === "REJECTED_PERMANENT" ? { error: { code: result.errorCode, message: result.reason } } : {}
1620
+ });
1621
+ }
1575
1622
  async #rejectFailedPrerequisite(operation) {
1576
1623
  if (operation.kind !== "checkIn" || this.#replayConfig === void 0) return false;
1577
1624
  const spot2 = this.#replayConfig.spots.find(
@@ -1579,9 +1626,10 @@ var OfflineQueue = class {
1579
1626
  );
1580
1627
  if (spot2 === void 0) return false;
1581
1628
  const failedSpots = new Set(
1582
- this.#rejectedHistory.filter((entry) => entry.operation.kind === "checkIn").map(
1583
- (entry) => entry.operation.kind === "checkIn" ? entry.operation.request.spotId : void 0
1584
- ).filter((spotId) => spotId !== void 0)
1629
+ [
1630
+ ...this.#rejectedHistory.map((entry) => entry.operation),
1631
+ ...this.#operations.filter((candidate) => candidate.status === "REJECTED_PERMANENT")
1632
+ ].filter((candidate) => candidate.kind === "checkIn").map((candidate) => candidate.kind === "checkIn" ? candidate.request.spotId : void 0).filter((spotId) => spotId !== void 0)
1585
1633
  );
1586
1634
  if (!spot2.prerequisites?.some((prerequisite) => failedSpots.has(prerequisite))) return false;
1587
1635
  const error = {
@@ -1624,20 +1672,6 @@ var OfflineQueue = class {
1624
1672
  try {
1625
1673
  this.#channel = new Channel("stamprally:queue-sync");
1626
1674
  this.#channel.addEventListener("message", (event) => {
1627
- if (typeof event.data === "object" && event.data !== null) {
1628
- const data = event.data;
1629
- if (data.type === "lock" && data.lockKey === this.#lockKey()) {
1630
- this.#observedLocks.set(data.lockKey, {
1631
- owner: typeof data.owner === "string" ? data.owner : "unknown",
1632
- expiresAt: typeof data.expiresAt === "number" ? data.expiresAt : 0
1633
- });
1634
- return;
1635
- }
1636
- if (data.type === "unlock" && data.lockKey === this.#lockKey()) {
1637
- this.#observedLocks.delete(data.lockKey);
1638
- return;
1639
- }
1640
- }
1641
1675
  if (typeof event.data === "object" && event.data !== null && "key" in event.data && event.data.key === this.#storageKey())
1642
1676
  void this.#reloadFromStorage();
1643
1677
  });
@@ -1664,57 +1698,6 @@ var OfflineQueue = class {
1664
1698
  } catch {
1665
1699
  }
1666
1700
  }
1667
- #lockKey() {
1668
- return `${this.#storageKey()}:sync-lock`;
1669
- }
1670
- #acquireSyncLock() {
1671
- const key = this.#lockKey();
1672
- const now = Date.now();
1673
- const local = syncLocks.get(key);
1674
- if (local !== void 0 && local.expiresAt > now && local.owner !== this.#instanceId)
1675
- return false;
1676
- const observed = this.#observedLocks.get(key);
1677
- if (observed !== void 0 && observed.expiresAt > now && observed.owner !== this.#instanceId)
1678
- return false;
1679
- if (this.#lockStorage !== null) {
1680
- try {
1681
- const existing = this.#lockStorage.getItem(key);
1682
- if (existing !== null) {
1683
- const parsed = JSON.parse(existing);
1684
- if (typeof parsed === "object" && parsed !== null && typeof parsed.expiresAt === "number" && parsed.expiresAt > now && parsed.owner !== this.#instanceId)
1685
- return false;
1686
- }
1687
- this.#lockStorage.setItem(
1688
- key,
1689
- JSON.stringify({ owner: this.#instanceId, expiresAt: now + SYNC_LOCK_TTL_MS })
1690
- );
1691
- this.#channel?.postMessage({
1692
- type: "lock",
1693
- lockKey: key,
1694
- owner: this.#instanceId,
1695
- expiresAt: now + SYNC_LOCK_TTL_MS
1696
- });
1697
- } catch {
1698
- this.#warnMemoryLock("Persistent lock access failed; offline sync is single-tab only.");
1699
- }
1700
- }
1701
- syncLocks.set(key, { owner: this.#instanceId, expiresAt: now + SYNC_LOCK_TTL_MS });
1702
- return true;
1703
- }
1704
- #releaseSyncLock() {
1705
- const key = this.#lockKey();
1706
- const current = syncLocks.get(key);
1707
- if (current?.owner === this.#instanceId) syncLocks.delete(key);
1708
- if (this.#lockStorage !== null) {
1709
- try {
1710
- const value = this.#lockStorage.getItem(key);
1711
- if (value !== null && JSON.parse(value).owner === this.#instanceId)
1712
- this.#lockStorage.removeItem?.(key);
1713
- this.#channel?.postMessage({ type: "unlock", lockKey: key, owner: this.#instanceId });
1714
- } catch {
1715
- }
1716
- }
1717
- }
1718
1701
  #storageKey() {
1719
1702
  if (this.#configuredKey !== void 0) return this.#configuredKey;
1720
1703
  return `stamprally:queue:${this.#rallyId ?? "unscoped"}:${this.#userId ?? "anonymous"}`;
@@ -1737,17 +1720,22 @@ var OfflineQueue = class {
1737
1720
  async #saveRejectedHistory() {
1738
1721
  await this.#storage.saveRejectedHistory?.(this.#storageKey(), this.#rejectedHistory);
1739
1722
  }
1740
- #warnMemoryLock(message) {
1741
- if (this.#warnedMemoryLock) return;
1742
- this.#warnedMemoryLock = true;
1723
+ #warnCapability(message) {
1724
+ if (this.#warnedCapabilityMessages.has(message)) return;
1725
+ this.#warnedCapabilityMessages.add(message);
1743
1726
  console.warn(`[@stamprally/core] ${message}`);
1744
1727
  this.#capabilityWarningListener?.({
1745
1728
  type: "STORAGE_CAPABILITY_WARNING",
1746
1729
  storageCapability: this.storageCapability === "memory" ? "memory" : "volatile_single_tab",
1730
+ multiTabSync: "disabled_unsafe_environment",
1747
1731
  isStoragePersistent: this.isStoragePersistent,
1748
1732
  message
1749
1733
  });
1750
1734
  }
1735
+ #hasWebLocks() {
1736
+ const locks = globalThis.navigator?.locks;
1737
+ return locks !== void 0 && typeof locks.request === "function";
1738
+ }
1751
1739
  };
1752
1740
  function normalizeOperation(operation) {
1753
1741
  const status = operation.status;
@@ -1792,18 +1780,18 @@ function applyInventoryDelta(state, previous, optimistic) {
1792
1780
  if (before !== void 0 && after !== void 0)
1793
1781
  rewardRemaining[key] = Math.max(0, (currentRewards[key] ?? before) + after - before);
1794
1782
  }
1795
- return {
1796
- ...state,
1797
- inventory: {
1798
- ...currentInventory.sharedRemaining === void 0 || sharedDelta === void 0 ? {} : { sharedRemaining: Math.max(0, currentInventory.sharedRemaining + sharedDelta) },
1799
- ...Object.keys(rewardRemaining).length === 0 ? {} : { rewardRemaining }
1800
- }
1783
+ const nextInventory = {
1784
+ ...currentInventory.sharedRemaining === void 0 || sharedDelta === void 0 ? optimisticInventory.sharedRemaining === void 0 || sharedDelta === void 0 ? {} : { sharedRemaining: Math.max(0, optimisticInventory.sharedRemaining) } : { sharedRemaining: Math.max(0, currentInventory.sharedRemaining + sharedDelta) },
1785
+ ...Object.keys(rewardRemaining).length === 0 ? {} : { rewardRemaining }
1801
1786
  };
1787
+ return { ...state, inventory: nextInventory };
1802
1788
  }
1803
- function applyOperation(state, operation, config) {
1789
+ function applyOperation(state, operation, config, rejectedCheckIns) {
1804
1790
  if (operation.kind === "checkIn") {
1805
1791
  const spot2 = config?.spots.find((candidate) => candidate.id === operation.request.spotId);
1806
- if (spot2?.prerequisites?.some((id2) => !state.records.some((record2) => record2.stampId === id2)))
1792
+ if (spot2?.prerequisites?.some(
1793
+ (id2) => rejectedCheckIns.has(id2) || !state.records.some((record2) => record2.stampId === id2)
1794
+ ))
1807
1795
  return { state, prerequisiteFailed: true };
1808
1796
  if (state.records.some((record2) => record2.stampId === operation.request.spotId))
1809
1797
  return { state, prerequisiteFailed: false };
@@ -1850,11 +1838,21 @@ function rebuildUserStateFromLog(baselineOrOptions, operationsArgument, configAr
1850
1838
  function rebuildUserStateLog(baseline, operations, config) {
1851
1839
  let state = cloneState(baseline);
1852
1840
  const rejectedOperationIds = [];
1841
+ const rejectedCheckIns = new Set(
1842
+ operations.filter(
1843
+ (operation) => operation.status === "REJECTED_PERMANENT" && operation.kind === "checkIn"
1844
+ ).map((operation) => operation.kind === "checkIn" ? operation.request.spotId : void 0).filter((spotId) => spotId !== void 0)
1845
+ );
1853
1846
  for (const operation of operations) {
1847
+ if (operation.status === "REJECTED_PERMANENT") {
1848
+ if (operation.kind === "checkIn") rejectedCheckIns.add(operation.request.spotId);
1849
+ continue;
1850
+ }
1854
1851
  if (!isReplayable(operation)) continue;
1855
- const replay = applyOperation(state, operation, config);
1852
+ const replay = applyOperation(state, operation, config, rejectedCheckIns);
1856
1853
  if (replay.prerequisiteFailed) {
1857
1854
  rejectedOperationIds.push(operationId(operation));
1855
+ if (operation.kind === "checkIn") rejectedCheckIns.add(operation.request.spotId);
1858
1856
  continue;
1859
1857
  }
1860
1858
  state = replay.state;
@@ -1902,9 +1900,36 @@ function emptyState(config, userId, now) {
1902
1900
  updatedAt: now
1903
1901
  };
1904
1902
  }
1903
+ function applyOptimisticRewardClaim(state, rewardId, reward2, now) {
1904
+ if (reward2.claimTicketNumber === void 0 || state.inventory === void 0)
1905
+ return {
1906
+ ...state,
1907
+ rewards: state.rewards.map((item) => item.rewardId === rewardId ? reward2 : item),
1908
+ updatedAt: now
1909
+ };
1910
+ const currentInventory = state.inventory;
1911
+ const currentRewardRemaining = currentInventory.rewardRemaining?.[rewardId];
1912
+ return {
1913
+ ...state,
1914
+ rewards: state.rewards.map((item) => item.rewardId === rewardId ? reward2 : item),
1915
+ updatedAt: now,
1916
+ inventory: {
1917
+ ...currentInventory.sharedRemaining === void 0 ? {} : { sharedRemaining: Math.max(0, currentInventory.sharedRemaining - 1) },
1918
+ ...currentInventory.rewardRemaining === void 0 || currentRewardRemaining === void 0 ? {} : {
1919
+ rewardRemaining: {
1920
+ ...currentInventory.rewardRemaining,
1921
+ [rewardId]: Math.max(0, currentRewardRemaining - 1)
1922
+ }
1923
+ }
1924
+ }
1925
+ };
1926
+ }
1905
1927
  function errorMessage(error, fallback) {
1906
1928
  return error !== void 0 && "message" in error && typeof error.message === "string" ? error.message : fallback;
1907
1929
  }
1930
+ function isSyncProgressResponse(value) {
1931
+ return typeof value === "object" && value !== null && "results" in value && Array.isArray(value.results) && "currentState" in value && "syncTimestamp" in value;
1932
+ }
1908
1933
  var StampRallyClient = class {
1909
1934
  #listeners = /* @__PURE__ */ new Set();
1910
1935
  #eventListeners = /* @__PURE__ */ new Set();
@@ -1929,6 +1954,9 @@ var StampRallyClient = class {
1929
1954
  this.#offlineQueue = this.#options.offlineQueue;
1930
1955
  this.#offlineQueue?.setReplayConfig(config);
1931
1956
  this.#offlineQueue?.setSyncResultListener((event) => this.#handleOfflineSyncResult(event));
1957
+ this.#offlineQueue?.setCapabilityWarningListener(
1958
+ (warning) => this.#emitEvent({ type: "storageCapabilityWarning", warning })
1959
+ );
1932
1960
  this.#offlineQueue?.setChangeListener(() => {
1933
1961
  this.#syncRevision += 1;
1934
1962
  if (this.#state !== null) this.#emit(this.#state);
@@ -1959,7 +1987,10 @@ var StampRallyClient = class {
1959
1987
  return this.#syncRevision;
1960
1988
  }
1961
1989
  get queueCapability() {
1962
- return this.#offlineQueue?.queueCapability ?? "custom";
1990
+ return this.#offlineQueue?.queueCapability ?? {
1991
+ storage: "custom",
1992
+ multiTabSync: "disabled_unsafe_environment"
1993
+ };
1963
1994
  }
1964
1995
  get storageCapability() {
1965
1996
  return this.#offlineQueue?.storageCapability ?? "custom";
@@ -2159,23 +2190,13 @@ var StampRallyClient = class {
2159
2190
  return result.ok ? this.#commitClaim(result) : this.#fail(result.error);
2160
2191
  } catch (error) {
2161
2192
  if (this.#offlineQueue === void 0) throw error;
2162
- const next2 = {
2163
- ...current,
2164
- rewards: current.rewards.map(
2165
- (item) => item.rewardId === rewardId ? local.value : item
2166
- ),
2167
- updatedAt: now
2168
- };
2193
+ const next2 = applyOptimisticRewardClaim(current, rewardId, local.value, now);
2169
2194
  await this.#offlineQueue.enqueueClaimReward(request, next2);
2170
2195
  await this.#storage.save(next2);
2171
2196
  return this.#commitClaim({ ok: true, value: { state: next2, reward: local.value } });
2172
2197
  }
2173
2198
  }
2174
- const next = {
2175
- ...current,
2176
- rewards: current.rewards.map((item) => item.rewardId === rewardId ? local.value : item),
2177
- updatedAt: now
2178
- };
2199
+ const next = applyOptimisticRewardClaim(current, rewardId, local.value, now);
2179
2200
  await this.#storage.save(next);
2180
2201
  return this.#commitClaim({ ok: true, value: { state: next, reward: local.value } });
2181
2202
  });
@@ -2186,7 +2207,9 @@ var StampRallyClient = class {
2186
2207
  this.#syncMetrics = { processed: 0, failed: 0 };
2187
2208
  try {
2188
2209
  const current = await this.initialize();
2189
- if (this.#offlineQueue !== void 0 && adapter !== void 0) {
2210
+ const queuedOperations = this.#offlineQueue?.operations ?? [];
2211
+ const hasPerOperationAdapters = adapter?.checkIn !== void 0 || adapter?.claimReward !== void 0;
2212
+ if (this.#offlineQueue !== void 0 && adapter !== void 0 && hasPerOperationAdapters) {
2190
2213
  await this.#offlineQueue.sync(async (operation) => {
2191
2214
  if (operation.kind === "checkIn") {
2192
2215
  if (adapter.checkIn === void 0)
@@ -2203,12 +2226,16 @@ var StampRallyClient = class {
2203
2226
  return;
2204
2227
  }
2205
2228
  const localState = this.#state ?? current;
2206
- const serverState = await adapter.sync({
2229
+ const syncResult = await adapter.sync({
2207
2230
  rallyId: this.#config.id,
2208
2231
  userId: this.#userId,
2209
2232
  state: localState,
2210
- ...this.#userId === this.#anonymousSessionId ? { anonymousSessionId: this.#anonymousSessionId } : {}
2233
+ ...this.#userId === this.#anonymousSessionId ? { anonymousSessionId: this.#anonymousSessionId } : {},
2234
+ ...this.#offlineQueue !== void 0 && !hasPerOperationAdapters && queuedOperations.length > 0 ? { operations: queuedOperations } : {}
2211
2235
  });
2236
+ const progress = isSyncProgressResponse(syncResult) ? syncResult : void 0;
2237
+ if (progress !== void 0) await this.#offlineQueue?.applySyncProgress(progress);
2238
+ const serverState = isSyncProgressResponse(syncResult) ? syncResult.currentState : syncResult;
2212
2239
  const resolved = rebuildUserStateFromLog(
2213
2240
  serverState,
2214
2241
  this.#offlineQueue?.operations ?? [],
@@ -3249,7 +3276,7 @@ function validateRallyConfigRelations(config) {
3249
3276
  ["stockKey", reward2.stockKey],
3250
3277
  ["secondaryStockKey", reward2.secondaryStockKey]
3251
3278
  ]) {
3252
- if (key !== void 0 && key !== "__shared__" && inventory?.[key] === void 0)
3279
+ if (key !== void 0 && (key === "__shared__" && inventory?.sharedStock === void 0 || key !== "__shared__" && inventory?.[key] === void 0))
3253
3280
  add(
3254
3281
  errors,
3255
3282
  `rewards[${index}].${field}`,
@@ -3257,13 +3284,6 @@ function validateRallyConfigRelations(config) {
3257
3284
  "missing_inventory_key"
3258
3285
  );
3259
3286
  }
3260
- if (reward2.stockKey === "__shared__" && inventory?.sharedStock === void 0)
3261
- add(
3262
- errors,
3263
- `rewards[${index}].stockKey`,
3264
- "sharedStock is not defined.",
3265
- "missing_inventory_key"
3266
- );
3267
3287
  });
3268
3288
  const visiting = /* @__PURE__ */ new Set();
3269
3289
  const visited = /* @__PURE__ */ new Set();