@stamprally/core 0.19.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 +29 -2
- package/dist/index.cjs +99 -100
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +111 -79
- package/dist/index.d.ts +111 -79
- package/dist/index.js +99 -100
- 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(
|
|
@@ -1625,20 +1672,6 @@ var OfflineQueue = class {
|
|
|
1625
1672
|
try {
|
|
1626
1673
|
this.#channel = new Channel("stamprally:queue-sync");
|
|
1627
1674
|
this.#channel.addEventListener("message", (event) => {
|
|
1628
|
-
if (typeof event.data === "object" && event.data !== null) {
|
|
1629
|
-
const data = event.data;
|
|
1630
|
-
if (data.type === "lock" && data.lockKey === this.#lockKey()) {
|
|
1631
|
-
this.#observedLocks.set(data.lockKey, {
|
|
1632
|
-
owner: typeof data.owner === "string" ? data.owner : "unknown",
|
|
1633
|
-
expiresAt: typeof data.expiresAt === "number" ? data.expiresAt : 0
|
|
1634
|
-
});
|
|
1635
|
-
return;
|
|
1636
|
-
}
|
|
1637
|
-
if (data.type === "unlock" && data.lockKey === this.#lockKey()) {
|
|
1638
|
-
this.#observedLocks.delete(data.lockKey);
|
|
1639
|
-
return;
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
1675
|
if (typeof event.data === "object" && event.data !== null && "key" in event.data && event.data.key === this.#storageKey())
|
|
1643
1676
|
void this.#reloadFromStorage();
|
|
1644
1677
|
});
|
|
@@ -1665,57 +1698,6 @@ var OfflineQueue = class {
|
|
|
1665
1698
|
} catch {
|
|
1666
1699
|
}
|
|
1667
1700
|
}
|
|
1668
|
-
#lockKey() {
|
|
1669
|
-
return `${this.#storageKey()}:sync-lock`;
|
|
1670
|
-
}
|
|
1671
|
-
#acquireSyncLock() {
|
|
1672
|
-
const key = this.#lockKey();
|
|
1673
|
-
const now = Date.now();
|
|
1674
|
-
const local = syncLocks.get(key);
|
|
1675
|
-
if (local !== void 0 && local.expiresAt > now && local.owner !== this.#instanceId)
|
|
1676
|
-
return false;
|
|
1677
|
-
const observed = this.#observedLocks.get(key);
|
|
1678
|
-
if (observed !== void 0 && observed.expiresAt > now && observed.owner !== this.#instanceId)
|
|
1679
|
-
return false;
|
|
1680
|
-
if (this.#lockStorage !== null) {
|
|
1681
|
-
try {
|
|
1682
|
-
const existing = this.#lockStorage.getItem(key);
|
|
1683
|
-
if (existing !== null) {
|
|
1684
|
-
const parsed = JSON.parse(existing);
|
|
1685
|
-
if (typeof parsed === "object" && parsed !== null && typeof parsed.expiresAt === "number" && parsed.expiresAt > now && parsed.owner !== this.#instanceId)
|
|
1686
|
-
return false;
|
|
1687
|
-
}
|
|
1688
|
-
this.#lockStorage.setItem(
|
|
1689
|
-
key,
|
|
1690
|
-
JSON.stringify({ owner: this.#instanceId, expiresAt: now + SYNC_LOCK_TTL_MS })
|
|
1691
|
-
);
|
|
1692
|
-
this.#channel?.postMessage({
|
|
1693
|
-
type: "lock",
|
|
1694
|
-
lockKey: key,
|
|
1695
|
-
owner: this.#instanceId,
|
|
1696
|
-
expiresAt: now + SYNC_LOCK_TTL_MS
|
|
1697
|
-
});
|
|
1698
|
-
} catch {
|
|
1699
|
-
this.#warnMemoryLock("Persistent lock access failed; offline sync is single-tab only.");
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
syncLocks.set(key, { owner: this.#instanceId, expiresAt: now + SYNC_LOCK_TTL_MS });
|
|
1703
|
-
return true;
|
|
1704
|
-
}
|
|
1705
|
-
#releaseSyncLock() {
|
|
1706
|
-
const key = this.#lockKey();
|
|
1707
|
-
const current = syncLocks.get(key);
|
|
1708
|
-
if (current?.owner === this.#instanceId) syncLocks.delete(key);
|
|
1709
|
-
if (this.#lockStorage !== null) {
|
|
1710
|
-
try {
|
|
1711
|
-
const value = this.#lockStorage.getItem(key);
|
|
1712
|
-
if (value !== null && JSON.parse(value).owner === this.#instanceId)
|
|
1713
|
-
this.#lockStorage.removeItem?.(key);
|
|
1714
|
-
this.#channel?.postMessage({ type: "unlock", lockKey: key, owner: this.#instanceId });
|
|
1715
|
-
} catch {
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
1701
|
#storageKey() {
|
|
1720
1702
|
if (this.#configuredKey !== void 0) return this.#configuredKey;
|
|
1721
1703
|
return `stamprally:queue:${this.#rallyId ?? "unscoped"}:${this.#userId ?? "anonymous"}`;
|
|
@@ -1738,17 +1720,22 @@ var OfflineQueue = class {
|
|
|
1738
1720
|
async #saveRejectedHistory() {
|
|
1739
1721
|
await this.#storage.saveRejectedHistory?.(this.#storageKey(), this.#rejectedHistory);
|
|
1740
1722
|
}
|
|
1741
|
-
#
|
|
1742
|
-
if (this.#
|
|
1743
|
-
this.#
|
|
1723
|
+
#warnCapability(message) {
|
|
1724
|
+
if (this.#warnedCapabilityMessages.has(message)) return;
|
|
1725
|
+
this.#warnedCapabilityMessages.add(message);
|
|
1744
1726
|
console.warn(`[@stamprally/core] ${message}`);
|
|
1745
1727
|
this.#capabilityWarningListener?.({
|
|
1746
1728
|
type: "STORAGE_CAPABILITY_WARNING",
|
|
1747
1729
|
storageCapability: this.storageCapability === "memory" ? "memory" : "volatile_single_tab",
|
|
1730
|
+
multiTabSync: "disabled_unsafe_environment",
|
|
1748
1731
|
isStoragePersistent: this.isStoragePersistent,
|
|
1749
1732
|
message
|
|
1750
1733
|
});
|
|
1751
1734
|
}
|
|
1735
|
+
#hasWebLocks() {
|
|
1736
|
+
const locks = globalThis.navigator?.locks;
|
|
1737
|
+
return locks !== void 0 && typeof locks.request === "function";
|
|
1738
|
+
}
|
|
1752
1739
|
};
|
|
1753
1740
|
function normalizeOperation(operation) {
|
|
1754
1741
|
const status = operation.status;
|
|
@@ -1940,6 +1927,9 @@ function applyOptimisticRewardClaim(state, rewardId, reward2, now) {
|
|
|
1940
1927
|
function errorMessage(error, fallback) {
|
|
1941
1928
|
return error !== void 0 && "message" in error && typeof error.message === "string" ? error.message : fallback;
|
|
1942
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
|
+
}
|
|
1943
1933
|
var StampRallyClient = class {
|
|
1944
1934
|
#listeners = /* @__PURE__ */ new Set();
|
|
1945
1935
|
#eventListeners = /* @__PURE__ */ new Set();
|
|
@@ -1997,7 +1987,10 @@ var StampRallyClient = class {
|
|
|
1997
1987
|
return this.#syncRevision;
|
|
1998
1988
|
}
|
|
1999
1989
|
get queueCapability() {
|
|
2000
|
-
return this.#offlineQueue?.queueCapability ??
|
|
1990
|
+
return this.#offlineQueue?.queueCapability ?? {
|
|
1991
|
+
storage: "custom",
|
|
1992
|
+
multiTabSync: "disabled_unsafe_environment"
|
|
1993
|
+
};
|
|
2001
1994
|
}
|
|
2002
1995
|
get storageCapability() {
|
|
2003
1996
|
return this.#offlineQueue?.storageCapability ?? "custom";
|
|
@@ -2214,7 +2207,9 @@ var StampRallyClient = class {
|
|
|
2214
2207
|
this.#syncMetrics = { processed: 0, failed: 0 };
|
|
2215
2208
|
try {
|
|
2216
2209
|
const current = await this.initialize();
|
|
2217
|
-
|
|
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) {
|
|
2218
2213
|
await this.#offlineQueue.sync(async (operation) => {
|
|
2219
2214
|
if (operation.kind === "checkIn") {
|
|
2220
2215
|
if (adapter.checkIn === void 0)
|
|
@@ -2231,12 +2226,16 @@ var StampRallyClient = class {
|
|
|
2231
2226
|
return;
|
|
2232
2227
|
}
|
|
2233
2228
|
const localState = this.#state ?? current;
|
|
2234
|
-
const
|
|
2229
|
+
const syncResult = await adapter.sync({
|
|
2235
2230
|
rallyId: this.#config.id,
|
|
2236
2231
|
userId: this.#userId,
|
|
2237
2232
|
state: localState,
|
|
2238
|
-
...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 } : {}
|
|
2239
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;
|
|
2240
2239
|
const resolved = rebuildUserStateFromLog(
|
|
2241
2240
|
serverState,
|
|
2242
2241
|
this.#offlineQueue?.operations ?? [],
|