@stamprally/core 0.15.0 → 0.16.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 +2 -2
- package/dist/index.cjs +237 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -5
- package/dist/index.d.ts +33 -5
- package/dist/index.js +237 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1135,6 +1135,18 @@ var StampRallyClient = class {
|
|
|
1135
1135
|
get pendingCount() {
|
|
1136
1136
|
return this.#offlineQueue?.pendingCount ?? 0;
|
|
1137
1137
|
}
|
|
1138
|
+
get rejectedHistory() {
|
|
1139
|
+
return this.#offlineQueue?.rejectedHistory ?? [];
|
|
1140
|
+
}
|
|
1141
|
+
get queueCapability() {
|
|
1142
|
+
return this.#offlineQueue?.queueCapability ?? "custom";
|
|
1143
|
+
}
|
|
1144
|
+
discardRejected(operationId) {
|
|
1145
|
+
return this.#offlineQueue?.discardRejected(operationId) ?? Promise.resolve(false);
|
|
1146
|
+
}
|
|
1147
|
+
retryRejected(operationId) {
|
|
1148
|
+
return this.#offlineQueue?.retryRejected(operationId) ?? Promise.resolve(false);
|
|
1149
|
+
}
|
|
1138
1150
|
subscribe(listener) {
|
|
1139
1151
|
this.#listeners.add(listener);
|
|
1140
1152
|
return () => this.#listeners.delete(listener);
|
|
@@ -1457,12 +1469,19 @@ var StampRallyClient = class {
|
|
|
1457
1469
|
// src/client/offlineQueue.ts
|
|
1458
1470
|
var MemoryQueueStorage = class {
|
|
1459
1471
|
#values = /* @__PURE__ */ new Map();
|
|
1472
|
+
#rejected = /* @__PURE__ */ new Map();
|
|
1460
1473
|
async load(key) {
|
|
1461
1474
|
return this.#values.get(key) ?? [];
|
|
1462
1475
|
}
|
|
1463
1476
|
async save(key, operations) {
|
|
1464
1477
|
this.#values.set(key, structuredClone(operations));
|
|
1465
1478
|
}
|
|
1479
|
+
async loadRejectedHistory(key) {
|
|
1480
|
+
return this.#rejected.get(key) ?? [];
|
|
1481
|
+
}
|
|
1482
|
+
async saveRejectedHistory(key, history) {
|
|
1483
|
+
this.#rejected.set(key, structuredClone(history));
|
|
1484
|
+
}
|
|
1466
1485
|
};
|
|
1467
1486
|
var LocalStorageQueueStorage = class {
|
|
1468
1487
|
constructor(storage) {
|
|
@@ -1482,6 +1501,19 @@ var LocalStorageQueueStorage = class {
|
|
|
1482
1501
|
async save(key, operations) {
|
|
1483
1502
|
this.storage.setItem(key, JSON.stringify(operations));
|
|
1484
1503
|
}
|
|
1504
|
+
async loadRejectedHistory(key) {
|
|
1505
|
+
const value = this.storage.getItem(`${key}:rejected-history`);
|
|
1506
|
+
if (value === null) return [];
|
|
1507
|
+
try {
|
|
1508
|
+
const parsed = JSON.parse(value);
|
|
1509
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1510
|
+
} catch {
|
|
1511
|
+
return [];
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
async saveRejectedHistory(key, history) {
|
|
1515
|
+
this.storage.setItem(`${key}:rejected-history`, JSON.stringify(history));
|
|
1516
|
+
}
|
|
1485
1517
|
};
|
|
1486
1518
|
var IndexedDBOfflineQueueStorage = class {
|
|
1487
1519
|
#providedFactory;
|
|
@@ -1509,6 +1541,26 @@ var IndexedDBOfflineQueueStorage = class {
|
|
|
1509
1541
|
transaction.onabort = () => reject(transaction.error ?? new Error("Offline queue write aborted."));
|
|
1510
1542
|
});
|
|
1511
1543
|
}
|
|
1544
|
+
async loadRejectedHistory(key) {
|
|
1545
|
+
const database = await this.#open();
|
|
1546
|
+
return new Promise((resolve, reject) => {
|
|
1547
|
+
const request = database.transaction("operations", "readonly").objectStore("operations").get(`${key}:rejected-history`);
|
|
1548
|
+
request.onsuccess = () => resolve(
|
|
1549
|
+
Array.isArray(request.result) ? request.result : []
|
|
1550
|
+
);
|
|
1551
|
+
request.onerror = () => reject(request.error ?? new Error("Failed to read rejected operation history."));
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
async saveRejectedHistory(key, history) {
|
|
1555
|
+
const database = await this.#open();
|
|
1556
|
+
return new Promise((resolve, reject) => {
|
|
1557
|
+
const transaction = database.transaction("operations", "readwrite");
|
|
1558
|
+
transaction.objectStore("operations").put(structuredClone(history), `${key}:rejected-history`);
|
|
1559
|
+
transaction.oncomplete = () => resolve();
|
|
1560
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("Failed to save rejected operation history."));
|
|
1561
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("Rejected operation history write aborted."));
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1512
1564
|
#open() {
|
|
1513
1565
|
if (this.#databasePromise !== null) return this.#databasePromise;
|
|
1514
1566
|
let factory = this.#providedFactory;
|
|
@@ -1531,22 +1583,35 @@ var IndexedDBOfflineQueueStorage = class {
|
|
|
1531
1583
|
return this.#databasePromise;
|
|
1532
1584
|
}
|
|
1533
1585
|
};
|
|
1586
|
+
function availableLocalStorage() {
|
|
1587
|
+
try {
|
|
1588
|
+
const storage = globalThis.localStorage;
|
|
1589
|
+
return storage ?? null;
|
|
1590
|
+
} catch {
|
|
1591
|
+
return null;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1534
1594
|
function defaultStorage(databaseName) {
|
|
1535
1595
|
try {
|
|
1536
1596
|
const indexedDB = globalThis.indexedDB;
|
|
1537
1597
|
if (indexedDB !== void 0)
|
|
1538
|
-
return
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1598
|
+
return {
|
|
1599
|
+
storage: new IndexedDBOfflineQueueStorage({
|
|
1600
|
+
indexedDB,
|
|
1601
|
+
...databaseName === void 0 ? {} : { databaseName }
|
|
1602
|
+
}),
|
|
1603
|
+
capability: "indexeddb"
|
|
1604
|
+
};
|
|
1605
|
+
const storage = availableLocalStorage();
|
|
1606
|
+
if (storage !== void 0 && storage !== null)
|
|
1607
|
+
return { storage: new LocalStorageQueueStorage(storage), capability: "localstorage" };
|
|
1544
1608
|
} catch {
|
|
1545
1609
|
}
|
|
1546
|
-
return new MemoryQueueStorage();
|
|
1610
|
+
return { storage: new MemoryQueueStorage(), capability: "memory" };
|
|
1547
1611
|
}
|
|
1548
|
-
function
|
|
1549
|
-
|
|
1612
|
+
function offlineOperationId(operation) {
|
|
1613
|
+
const identity = operation.request.userId ?? operation.request.anonymousSessionId ?? "anonymous";
|
|
1614
|
+
return operation.kind === "checkIn" ? `checkIn:${operation.request.rallyId}:${identity}:${operation.request.idempotencyKey}` : `claimReward:${operation.request.rallyId}:${identity}:${operation.request.idempotencyKey}`;
|
|
1550
1615
|
}
|
|
1551
1616
|
function requestScope(operation) {
|
|
1552
1617
|
return {
|
|
@@ -1576,12 +1641,14 @@ function randomId() {
|
|
|
1576
1641
|
}
|
|
1577
1642
|
var OfflineQueue = class {
|
|
1578
1643
|
#storage;
|
|
1644
|
+
#queueCapability;
|
|
1579
1645
|
#configuredKey;
|
|
1580
1646
|
#rallyId;
|
|
1581
1647
|
#userId;
|
|
1582
1648
|
#conflictPolicy;
|
|
1583
1649
|
#onSyncConflict;
|
|
1584
1650
|
#operations = [];
|
|
1651
|
+
#rejectedHistory = [];
|
|
1585
1652
|
#loaded = false;
|
|
1586
1653
|
#state = "idle";
|
|
1587
1654
|
#error = null;
|
|
@@ -1592,13 +1659,22 @@ var OfflineQueue = class {
|
|
|
1592
1659
|
#retryOptions;
|
|
1593
1660
|
#instanceId = randomId();
|
|
1594
1661
|
#lockStorage;
|
|
1662
|
+
#observedLocks = /* @__PURE__ */ new Map();
|
|
1663
|
+
#warnedMemoryLock = false;
|
|
1595
1664
|
#storageListener;
|
|
1596
1665
|
#channel = null;
|
|
1597
1666
|
constructor(options = {}) {
|
|
1598
|
-
if (options.storage !== void 0)
|
|
1599
|
-
|
|
1667
|
+
if (options.storage !== void 0) {
|
|
1668
|
+
this.#storage = options.storage;
|
|
1669
|
+
this.#queueCapability = "custom";
|
|
1670
|
+
} else if (options.storageLike !== void 0 && options.storageLike !== null) {
|
|
1600
1671
|
this.#storage = new LocalStorageQueueStorage(options.storageLike);
|
|
1601
|
-
|
|
1672
|
+
this.#queueCapability = "localstorage";
|
|
1673
|
+
} else {
|
|
1674
|
+
const selected = defaultStorage(options.databaseName);
|
|
1675
|
+
this.#storage = selected.storage;
|
|
1676
|
+
this.#queueCapability = selected.capability;
|
|
1677
|
+
}
|
|
1602
1678
|
this.#configuredKey = options.key;
|
|
1603
1679
|
this.#rallyId = options.rallyId;
|
|
1604
1680
|
this.#userId = options.userId ?? null;
|
|
@@ -1614,7 +1690,7 @@ var OfflineQueue = class {
|
|
|
1614
1690
|
initialIntervalMs: Math.max(0, retryOptions.initialIntervalMs),
|
|
1615
1691
|
backoffMultiplier: Math.max(1, retryOptions.backoffMultiplier)
|
|
1616
1692
|
};
|
|
1617
|
-
this.#lockStorage = options.storageLike ??
|
|
1693
|
+
this.#lockStorage = options.storageLike ?? availableLocalStorage();
|
|
1618
1694
|
if (this.#synchronizeInstances) this.#subscribeToExternalChanges();
|
|
1619
1695
|
}
|
|
1620
1696
|
get syncState() {
|
|
@@ -1623,6 +1699,12 @@ var OfflineQueue = class {
|
|
|
1623
1699
|
get pendingCount() {
|
|
1624
1700
|
return this.#operations.length;
|
|
1625
1701
|
}
|
|
1702
|
+
get queueCapability() {
|
|
1703
|
+
return this.#queueCapability;
|
|
1704
|
+
}
|
|
1705
|
+
get rejectedHistory() {
|
|
1706
|
+
return this.#rejectedHistory;
|
|
1707
|
+
}
|
|
1626
1708
|
get error() {
|
|
1627
1709
|
return this.#error;
|
|
1628
1710
|
}
|
|
@@ -1646,7 +1728,20 @@ var OfflineQueue = class {
|
|
|
1646
1728
|
}
|
|
1647
1729
|
async initialize() {
|
|
1648
1730
|
if (this.#loaded) return;
|
|
1649
|
-
|
|
1731
|
+
try {
|
|
1732
|
+
const key = this.#storageKey();
|
|
1733
|
+
this.#operations = (await this.#storage.load(key)).map(normalizeOperation);
|
|
1734
|
+
this.#rejectedHistory = (await this.#storage.loadRejectedHistory?.(key))?.map(normalizeRejectedHistory) ?? [];
|
|
1735
|
+
} catch (error) {
|
|
1736
|
+
if (this.#queueCapability === "memory") throw error;
|
|
1737
|
+
this.#storage = new MemoryQueueStorage();
|
|
1738
|
+
this.#queueCapability = "memory";
|
|
1739
|
+
this.#operations = [];
|
|
1740
|
+
this.#rejectedHistory = [];
|
|
1741
|
+
this.#warnMemoryLock(
|
|
1742
|
+
`Offline queue persistence is unavailable; queued data can be lost after reload (${String(error)}).`
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1650
1745
|
this.#loaded = true;
|
|
1651
1746
|
}
|
|
1652
1747
|
/** Releases browser listeners when the queue is no longer used. */
|
|
@@ -1689,8 +1784,8 @@ var OfflineQueue = class {
|
|
|
1689
1784
|
throw new Error("Offline operation belongs to another rally or user queue.");
|
|
1690
1785
|
}
|
|
1691
1786
|
await this.initialize();
|
|
1692
|
-
const id2 =
|
|
1693
|
-
if (this.#operations.some((item) =>
|
|
1787
|
+
const id2 = offlineOperationId(operation);
|
|
1788
|
+
if (this.#operations.some((item) => offlineOperationId(item) === id2)) return;
|
|
1694
1789
|
this.#operations = [...this.#operations, { ...operation, status: "PENDING", attempts: 0 }];
|
|
1695
1790
|
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1696
1791
|
this.#announceChange();
|
|
@@ -1707,6 +1802,47 @@ var OfflineQueue = class {
|
|
|
1707
1802
|
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1708
1803
|
this.#announceChange();
|
|
1709
1804
|
}
|
|
1805
|
+
async discardRejected(operationId) {
|
|
1806
|
+
await this.initialize();
|
|
1807
|
+
const next = this.#rejectedHistory.filter(
|
|
1808
|
+
(entry) => offlineOperationId(entry.operation) !== operationId
|
|
1809
|
+
);
|
|
1810
|
+
if (next.length === this.#rejectedHistory.length) return false;
|
|
1811
|
+
this.#rejectedHistory = next;
|
|
1812
|
+
await this.#saveRejectedHistory();
|
|
1813
|
+
this.#announceChange();
|
|
1814
|
+
return true;
|
|
1815
|
+
}
|
|
1816
|
+
async retryRejected(operationId) {
|
|
1817
|
+
await this.initialize();
|
|
1818
|
+
const entry = this.#rejectedHistory.find(
|
|
1819
|
+
(candidate) => offlineOperationId(candidate.operation) === operationId
|
|
1820
|
+
);
|
|
1821
|
+
if (entry === void 0) return false;
|
|
1822
|
+
if (!this.#operations.some((operation) => offlineOperationId(operation) === operationId))
|
|
1823
|
+
this.#operations = [
|
|
1824
|
+
...this.#operations,
|
|
1825
|
+
{ ...entry.operation, status: "PENDING", attempts: 0 }
|
|
1826
|
+
];
|
|
1827
|
+
this.#rejectedHistory = this.#rejectedHistory.filter((candidate) => candidate !== entry);
|
|
1828
|
+
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1829
|
+
await this.#saveRejectedHistory();
|
|
1830
|
+
this.#announceChange();
|
|
1831
|
+
return true;
|
|
1832
|
+
}
|
|
1833
|
+
async discardRejectedOperation(operationId) {
|
|
1834
|
+
return this.discardRejected(operationId);
|
|
1835
|
+
}
|
|
1836
|
+
async retryRejectedOperation(operationId) {
|
|
1837
|
+
return this.retryRejected(operationId);
|
|
1838
|
+
}
|
|
1839
|
+
async clearRejectedHistory() {
|
|
1840
|
+
await this.initialize();
|
|
1841
|
+
if (this.#rejectedHistory.length === 0) return;
|
|
1842
|
+
this.#rejectedHistory = [];
|
|
1843
|
+
await this.#saveRejectedHistory();
|
|
1844
|
+
this.#announceChange();
|
|
1845
|
+
}
|
|
1710
1846
|
async sync(sender = this.#sender) {
|
|
1711
1847
|
await this.initialize();
|
|
1712
1848
|
if (sender === void 0) throw new Error("OfflineQueue.sync requires a sender.");
|
|
@@ -1722,7 +1858,7 @@ var OfflineQueue = class {
|
|
|
1722
1858
|
}
|
|
1723
1859
|
async #run(sender) {
|
|
1724
1860
|
const locks = globalThis.navigator?.locks;
|
|
1725
|
-
if (locks !== void 0) {
|
|
1861
|
+
if (locks !== void 0 && typeof locks.request === "function") {
|
|
1726
1862
|
let callbackStarted = false;
|
|
1727
1863
|
try {
|
|
1728
1864
|
const acquired = await locks.request(
|
|
@@ -1745,6 +1881,10 @@ var OfflineQueue = class {
|
|
|
1745
1881
|
if (callbackStarted) throw error;
|
|
1746
1882
|
}
|
|
1747
1883
|
}
|
|
1884
|
+
if (this.#lockStorage === null)
|
|
1885
|
+
this.#warnMemoryLock(
|
|
1886
|
+
"No cross-tab storage lock is available; offline sync is single-tab only."
|
|
1887
|
+
);
|
|
1748
1888
|
await this.#runWithStorageLock(sender);
|
|
1749
1889
|
}
|
|
1750
1890
|
async #runWithStorageLock(sender) {
|
|
@@ -1775,20 +1915,38 @@ var OfflineQueue = class {
|
|
|
1775
1915
|
const error2 = errorValue(response.error ?? response.reason, "RETRYABLE_ERROR");
|
|
1776
1916
|
await this.#updateOperationStatus("PENDING", attempt + 1);
|
|
1777
1917
|
await this.#syncResultListener?.({ operation, status: response.status, error: error2 });
|
|
1778
|
-
if (attempt >= this.#retryOptions.maxRetries)
|
|
1918
|
+
if (attempt >= this.#retryOptions.maxRetries) {
|
|
1919
|
+
await this.#updateOperationStatus("FAILED_RETRYABLE", attempt + 1);
|
|
1920
|
+
throw new Error(error2.message);
|
|
1921
|
+
}
|
|
1779
1922
|
const interval = this.#retryOptions.initialIntervalMs * this.#retryOptions.backoffMultiplier ** attempt;
|
|
1780
1923
|
await new Promise((resolve) => setTimeout(resolve, Math.max(0, interval)));
|
|
1781
1924
|
attempt += 1;
|
|
1782
1925
|
}
|
|
1783
1926
|
const result = response.result;
|
|
1784
|
-
const state = response.state ?? (result !== void 0 && "conflict" in result && result.conflict === true ?
|
|
1785
|
-
const error = response.status === "REJECTED_PERMANENT" ? errorValue(
|
|
1927
|
+
const state = response.state ?? (result !== void 0 && "conflict" in result && result.conflict === true ? result.serverState : result !== void 0 && "ok" in result && result.ok ? result.value.state : void 0);
|
|
1928
|
+
const error = response.status === "REJECTED_PERMANENT" ? errorValue(
|
|
1929
|
+
response.error ?? response.reason ?? (result !== void 0 && "ok" in result && !result.ok ? result.error : void 0),
|
|
1930
|
+
"REJECTED_PERMANENT"
|
|
1931
|
+
) : void 0;
|
|
1786
1932
|
await this.#updateOperationStatus(
|
|
1787
|
-
response.status === "ACCEPTED" ? "ACCEPTED" : "
|
|
1933
|
+
response.status === "ACCEPTED" ? "ACCEPTED" : "REJECTED_PERMANENT",
|
|
1788
1934
|
attempt + 1
|
|
1789
1935
|
);
|
|
1790
|
-
const
|
|
1791
|
-
|
|
1936
|
+
const eventState = state;
|
|
1937
|
+
if (response.status === "REJECTED_PERMANENT" && error !== void 0) {
|
|
1938
|
+
this.#rejectedHistory = [
|
|
1939
|
+
...this.#rejectedHistory,
|
|
1940
|
+
{
|
|
1941
|
+
operation: { ...operation, status: "REJECTED_PERMANENT", attempts: attempt + 1 },
|
|
1942
|
+
reason: error,
|
|
1943
|
+
errorCode: error.code,
|
|
1944
|
+
rejectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1945
|
+
attempts: attempt + 1
|
|
1946
|
+
}
|
|
1947
|
+
];
|
|
1948
|
+
await this.#saveRejectedHistory();
|
|
1949
|
+
}
|
|
1792
1950
|
this.#operations = this.#operations.slice(1);
|
|
1793
1951
|
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1794
1952
|
this.#announceChange();
|
|
@@ -1820,7 +1978,8 @@ var OfflineQueue = class {
|
|
|
1820
1978
|
const windowLike = globalThis.window;
|
|
1821
1979
|
if (windowLike !== void 0) {
|
|
1822
1980
|
this.#storageListener = (event) => {
|
|
1823
|
-
if (event.key === this.#storageKey()
|
|
1981
|
+
if (event.key === this.#storageKey() || event.key === `${this.#storageKey()}:rejected-history`)
|
|
1982
|
+
void this.#reloadFromStorage();
|
|
1824
1983
|
};
|
|
1825
1984
|
windowLike.addEventListener("storage", this.#storageListener);
|
|
1826
1985
|
}
|
|
@@ -1829,6 +1988,20 @@ var OfflineQueue = class {
|
|
|
1829
1988
|
try {
|
|
1830
1989
|
this.#channel = new Channel("stamprally:queue-sync");
|
|
1831
1990
|
this.#channel.addEventListener("message", (event) => {
|
|
1991
|
+
if (typeof event.data === "object" && event.data !== null) {
|
|
1992
|
+
const data = event.data;
|
|
1993
|
+
if (data.type === "lock" && data.lockKey === this.#lockKey()) {
|
|
1994
|
+
this.#observedLocks.set(data.lockKey, {
|
|
1995
|
+
owner: typeof data.owner === "string" ? data.owner : "unknown",
|
|
1996
|
+
expiresAt: typeof data.expiresAt === "number" ? data.expiresAt : 0
|
|
1997
|
+
});
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
if (data.type === "unlock" && data.lockKey === this.#lockKey()) {
|
|
2001
|
+
this.#observedLocks.delete(data.lockKey);
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
1832
2005
|
if (typeof event.data === "object" && event.data !== null && "key" in event.data && event.data.key === this.#storageKey())
|
|
1833
2006
|
void this.#reloadFromStorage();
|
|
1834
2007
|
});
|
|
@@ -1838,12 +2011,18 @@ var OfflineQueue = class {
|
|
|
1838
2011
|
}
|
|
1839
2012
|
}
|
|
1840
2013
|
#announceChange() {
|
|
1841
|
-
this.#channel?.postMessage({
|
|
2014
|
+
this.#channel?.postMessage({
|
|
2015
|
+
type: "change",
|
|
2016
|
+
key: this.#storageKey(),
|
|
2017
|
+
owner: this.#instanceId
|
|
2018
|
+
});
|
|
1842
2019
|
}
|
|
1843
2020
|
async #reloadFromStorage() {
|
|
1844
2021
|
if (this.#state === "syncing") return;
|
|
1845
2022
|
try {
|
|
1846
|
-
|
|
2023
|
+
const key = this.#storageKey();
|
|
2024
|
+
this.#operations = (await this.#storage.load(key)).map(normalizeOperation);
|
|
2025
|
+
this.#rejectedHistory = (await this.#storage.loadRejectedHistory?.(key))?.map(normalizeRejectedHistory) ?? this.#rejectedHistory;
|
|
1847
2026
|
this.#loaded = true;
|
|
1848
2027
|
} catch {
|
|
1849
2028
|
}
|
|
@@ -1857,6 +2036,9 @@ var OfflineQueue = class {
|
|
|
1857
2036
|
const local = syncLocks.get(key);
|
|
1858
2037
|
if (local !== void 0 && local.expiresAt > now && local.owner !== this.#instanceId)
|
|
1859
2038
|
return false;
|
|
2039
|
+
const observed = this.#observedLocks.get(key);
|
|
2040
|
+
if (observed !== void 0 && observed.expiresAt > now && observed.owner !== this.#instanceId)
|
|
2041
|
+
return false;
|
|
1860
2042
|
if (this.#lockStorage !== null) {
|
|
1861
2043
|
try {
|
|
1862
2044
|
const existing = this.#lockStorage.getItem(key);
|
|
@@ -1869,7 +2051,14 @@ var OfflineQueue = class {
|
|
|
1869
2051
|
key,
|
|
1870
2052
|
JSON.stringify({ owner: this.#instanceId, expiresAt: now + SYNC_LOCK_TTL_MS })
|
|
1871
2053
|
);
|
|
2054
|
+
this.#channel?.postMessage({
|
|
2055
|
+
type: "lock",
|
|
2056
|
+
lockKey: key,
|
|
2057
|
+
owner: this.#instanceId,
|
|
2058
|
+
expiresAt: now + SYNC_LOCK_TTL_MS
|
|
2059
|
+
});
|
|
1872
2060
|
} catch {
|
|
2061
|
+
this.#warnMemoryLock("Persistent lock access failed; offline sync is single-tab only.");
|
|
1873
2062
|
}
|
|
1874
2063
|
}
|
|
1875
2064
|
syncLocks.set(key, { owner: this.#instanceId, expiresAt: now + SYNC_LOCK_TTL_MS });
|
|
@@ -1884,6 +2073,7 @@ var OfflineQueue = class {
|
|
|
1884
2073
|
const value = this.#lockStorage.getItem(key);
|
|
1885
2074
|
if (value !== null && JSON.parse(value).owner === this.#instanceId)
|
|
1886
2075
|
this.#lockStorage.removeItem?.(key);
|
|
2076
|
+
this.#channel?.postMessage({ type: "unlock", lockKey: key, owner: this.#instanceId });
|
|
1887
2077
|
} catch {
|
|
1888
2078
|
}
|
|
1889
2079
|
}
|
|
@@ -1912,14 +2102,33 @@ var OfflineQueue = class {
|
|
|
1912
2102
|
const policy = (typeof configured === "function" ? await configured({ operation, localState, serverState }) : configured) ?? this.#conflictPolicy;
|
|
1913
2103
|
return resolveRallyStateConflict(serverState, localState, { policy });
|
|
1914
2104
|
}
|
|
2105
|
+
async #saveRejectedHistory() {
|
|
2106
|
+
await this.#storage.saveRejectedHistory?.(this.#storageKey(), this.#rejectedHistory);
|
|
2107
|
+
}
|
|
2108
|
+
#warnMemoryLock(message) {
|
|
2109
|
+
if (this.#warnedMemoryLock) return;
|
|
2110
|
+
this.#warnedMemoryLock = true;
|
|
2111
|
+
console.warn(`[@stamprally/core] ${message}`);
|
|
2112
|
+
}
|
|
1915
2113
|
};
|
|
1916
2114
|
function normalizeOperation(operation) {
|
|
2115
|
+
const status = operation.status;
|
|
1917
2116
|
return {
|
|
1918
2117
|
...operation,
|
|
1919
|
-
status:
|
|
2118
|
+
status: status === "IN_FLIGHT" || status === "REJECTED" ? "PENDING" : status === "RETRYABLE_ERROR" ? "FAILED_RETRYABLE" : operation.status ?? "PENDING",
|
|
1920
2119
|
attempts: operation.attempts ?? 0
|
|
1921
2120
|
};
|
|
1922
2121
|
}
|
|
2122
|
+
function normalizeRejectedHistory(entry) {
|
|
2123
|
+
return {
|
|
2124
|
+
...entry,
|
|
2125
|
+
operation: normalizeOperation(entry.operation),
|
|
2126
|
+
reason: errorValue(entry.reason, "REJECTED_PERMANENT"),
|
|
2127
|
+
errorCode: entry.errorCode || entry.reason.code,
|
|
2128
|
+
rejectedAt: entry.rejectedAt || (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2129
|
+
attempts: entry.attempts ?? entry.operation.attempts ?? 0
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
1923
2132
|
|
|
1924
2133
|
// src/crypto/token.ts
|
|
1925
2134
|
var encoder = new TextEncoder();
|
|
@@ -2900,6 +3109,6 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
|
|
|
2900
3109
|
}
|
|
2901
3110
|
}
|
|
2902
3111
|
|
|
2903
|
-
export { ConfigValidationError, DEFAULT_SHEET_THEME, MemoryQueueStorage as InMemoryOfflineQueueStorage, InMemoryStorage, IndexedDBAdapter, IndexedDBOfflineQueueStorage, LocalStorageAdapter, OfflineQueue, StampRallyClient, StorageAdapterError, THEME_PRESETS, assertPublicConfig, calculateDistanceMeters, calculateProgress, consumeReward, createAnonymousSessionId, createClaimTicketNumber, createSecureToken, createSignedSnapshotToken, createUniqueClaimTicketNumber, evaluateCondition, evaluateConditionDetailed, evaluateSpotStatus, exportProgressToken, getCurrentGeoContext, getOrderedSpots, getSpotStatus, importProgressToken, isGeolocationSupported, isNfcSupported, isPublicConfig, isQrSupported, isRewardState, isStampRallyState, issueClaimTicketNumber, normalizePasscode, parseAdminConfig, parsePublicConfig, processStamp, readNfcContext, readQrContext, reconcileRewardStates, resolveLocalizedText, resolveRallyStateConflict, safeParseAdminConfig, safeParsePublicConfig, sanitizeAdminConfig, storageKey, toLocalizedString, toPublicConfig, updateLocalizedField, validatePublicConfigSafety, validateRallyConfigRelations, verifyPasscode, verifySecureToken, verifySnapshotToken };
|
|
3112
|
+
export { ConfigValidationError, DEFAULT_SHEET_THEME, MemoryQueueStorage as InMemoryOfflineQueueStorage, InMemoryStorage, IndexedDBAdapter, IndexedDBOfflineQueueStorage, LocalStorageAdapter, OfflineQueue, StampRallyClient, StorageAdapterError, THEME_PRESETS, assertPublicConfig, calculateDistanceMeters, calculateProgress, consumeReward, createAnonymousSessionId, createClaimTicketNumber, createSecureToken, createSignedSnapshotToken, createUniqueClaimTicketNumber, evaluateCondition, evaluateConditionDetailed, evaluateSpotStatus, exportProgressToken, getCurrentGeoContext, getOrderedSpots, getSpotStatus, importProgressToken, isGeolocationSupported, isNfcSupported, isPublicConfig, isQrSupported, isRewardState, isStampRallyState, issueClaimTicketNumber, normalizePasscode, offlineOperationId, parseAdminConfig, parsePublicConfig, processStamp, readNfcContext, readQrContext, reconcileRewardStates, resolveLocalizedText, resolveRallyStateConflict, safeParseAdminConfig, safeParsePublicConfig, sanitizeAdminConfig, storageKey, toLocalizedString, toPublicConfig, updateLocalizedField, validatePublicConfigSafety, validateRallyConfigRelations, verifyPasscode, verifySecureToken, verifySnapshotToken };
|
|
2904
3113
|
//# sourceMappingURL=index.js.map
|
|
2905
3114
|
//# sourceMappingURL=index.js.map
|