@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/README.md +30 -4
- package/dist/index.cjs +152 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -79
- package/dist/index.d.ts +114 -79
- package/dist/index.js +152 -132
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
#
|
|
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.#
|
|
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
|
|
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
|
-
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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
|
-
|
|
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 #
|
|
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
|
-
|
|
1583
|
-
(entry) => entry.operation
|
|
1584
|
-
|
|
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
|
-
#
|
|
1741
|
-
if (this.#
|
|
1742
|
-
this.#
|
|
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
|
-
|
|
1796
|
-
...
|
|
1797
|
-
|
|
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(
|
|
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 ??
|
|
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
|
-
|
|
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
|
|
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();
|