@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/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @stamprally/core v0.
|
|
1
|
+
# @stamprally/core v0.16.0
|
|
2
2
|
|
|
3
3
|
Dependency-free domain models, immutable state transitions, storage adapters, browser detectors, and safe configuration parsers.
|
|
4
4
|
|
|
@@ -7,7 +7,7 @@ import { InMemoryStorage, StampRallyClient, type PublicRallyConfig } from "@stam
|
|
|
7
7
|
|
|
8
8
|
const config: PublicRallyConfig = {
|
|
9
9
|
id: "city-tour",
|
|
10
|
-
version: "0.
|
|
10
|
+
version: "0.16.0",
|
|
11
11
|
title: "City Tour",
|
|
12
12
|
spots: [{ id: "station", orderIndex: 0, name: "Central Station", conditions: [{ type: "passcode" }] }],
|
|
13
13
|
rewards: [],
|
package/dist/index.cjs
CHANGED
|
@@ -1137,6 +1137,18 @@ var StampRallyClient = class {
|
|
|
1137
1137
|
get pendingCount() {
|
|
1138
1138
|
return this.#offlineQueue?.pendingCount ?? 0;
|
|
1139
1139
|
}
|
|
1140
|
+
get rejectedHistory() {
|
|
1141
|
+
return this.#offlineQueue?.rejectedHistory ?? [];
|
|
1142
|
+
}
|
|
1143
|
+
get queueCapability() {
|
|
1144
|
+
return this.#offlineQueue?.queueCapability ?? "custom";
|
|
1145
|
+
}
|
|
1146
|
+
discardRejected(operationId) {
|
|
1147
|
+
return this.#offlineQueue?.discardRejected(operationId) ?? Promise.resolve(false);
|
|
1148
|
+
}
|
|
1149
|
+
retryRejected(operationId) {
|
|
1150
|
+
return this.#offlineQueue?.retryRejected(operationId) ?? Promise.resolve(false);
|
|
1151
|
+
}
|
|
1140
1152
|
subscribe(listener) {
|
|
1141
1153
|
this.#listeners.add(listener);
|
|
1142
1154
|
return () => this.#listeners.delete(listener);
|
|
@@ -1459,12 +1471,19 @@ var StampRallyClient = class {
|
|
|
1459
1471
|
// src/client/offlineQueue.ts
|
|
1460
1472
|
var MemoryQueueStorage = class {
|
|
1461
1473
|
#values = /* @__PURE__ */ new Map();
|
|
1474
|
+
#rejected = /* @__PURE__ */ new Map();
|
|
1462
1475
|
async load(key) {
|
|
1463
1476
|
return this.#values.get(key) ?? [];
|
|
1464
1477
|
}
|
|
1465
1478
|
async save(key, operations) {
|
|
1466
1479
|
this.#values.set(key, structuredClone(operations));
|
|
1467
1480
|
}
|
|
1481
|
+
async loadRejectedHistory(key) {
|
|
1482
|
+
return this.#rejected.get(key) ?? [];
|
|
1483
|
+
}
|
|
1484
|
+
async saveRejectedHistory(key, history) {
|
|
1485
|
+
this.#rejected.set(key, structuredClone(history));
|
|
1486
|
+
}
|
|
1468
1487
|
};
|
|
1469
1488
|
var LocalStorageQueueStorage = class {
|
|
1470
1489
|
constructor(storage) {
|
|
@@ -1484,6 +1503,19 @@ var LocalStorageQueueStorage = class {
|
|
|
1484
1503
|
async save(key, operations) {
|
|
1485
1504
|
this.storage.setItem(key, JSON.stringify(operations));
|
|
1486
1505
|
}
|
|
1506
|
+
async loadRejectedHistory(key) {
|
|
1507
|
+
const value = this.storage.getItem(`${key}:rejected-history`);
|
|
1508
|
+
if (value === null) return [];
|
|
1509
|
+
try {
|
|
1510
|
+
const parsed = JSON.parse(value);
|
|
1511
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1512
|
+
} catch {
|
|
1513
|
+
return [];
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
async saveRejectedHistory(key, history) {
|
|
1517
|
+
this.storage.setItem(`${key}:rejected-history`, JSON.stringify(history));
|
|
1518
|
+
}
|
|
1487
1519
|
};
|
|
1488
1520
|
var IndexedDBOfflineQueueStorage = class {
|
|
1489
1521
|
#providedFactory;
|
|
@@ -1511,6 +1543,26 @@ var IndexedDBOfflineQueueStorage = class {
|
|
|
1511
1543
|
transaction.onabort = () => reject(transaction.error ?? new Error("Offline queue write aborted."));
|
|
1512
1544
|
});
|
|
1513
1545
|
}
|
|
1546
|
+
async loadRejectedHistory(key) {
|
|
1547
|
+
const database = await this.#open();
|
|
1548
|
+
return new Promise((resolve, reject) => {
|
|
1549
|
+
const request = database.transaction("operations", "readonly").objectStore("operations").get(`${key}:rejected-history`);
|
|
1550
|
+
request.onsuccess = () => resolve(
|
|
1551
|
+
Array.isArray(request.result) ? request.result : []
|
|
1552
|
+
);
|
|
1553
|
+
request.onerror = () => reject(request.error ?? new Error("Failed to read rejected operation history."));
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
async saveRejectedHistory(key, history) {
|
|
1557
|
+
const database = await this.#open();
|
|
1558
|
+
return new Promise((resolve, reject) => {
|
|
1559
|
+
const transaction = database.transaction("operations", "readwrite");
|
|
1560
|
+
transaction.objectStore("operations").put(structuredClone(history), `${key}:rejected-history`);
|
|
1561
|
+
transaction.oncomplete = () => resolve();
|
|
1562
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("Failed to save rejected operation history."));
|
|
1563
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("Rejected operation history write aborted."));
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1514
1566
|
#open() {
|
|
1515
1567
|
if (this.#databasePromise !== null) return this.#databasePromise;
|
|
1516
1568
|
let factory = this.#providedFactory;
|
|
@@ -1533,22 +1585,35 @@ var IndexedDBOfflineQueueStorage = class {
|
|
|
1533
1585
|
return this.#databasePromise;
|
|
1534
1586
|
}
|
|
1535
1587
|
};
|
|
1588
|
+
function availableLocalStorage() {
|
|
1589
|
+
try {
|
|
1590
|
+
const storage = globalThis.localStorage;
|
|
1591
|
+
return storage ?? null;
|
|
1592
|
+
} catch {
|
|
1593
|
+
return null;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1536
1596
|
function defaultStorage(databaseName) {
|
|
1537
1597
|
try {
|
|
1538
1598
|
const indexedDB = globalThis.indexedDB;
|
|
1539
1599
|
if (indexedDB !== void 0)
|
|
1540
|
-
return
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1600
|
+
return {
|
|
1601
|
+
storage: new IndexedDBOfflineQueueStorage({
|
|
1602
|
+
indexedDB,
|
|
1603
|
+
...databaseName === void 0 ? {} : { databaseName }
|
|
1604
|
+
}),
|
|
1605
|
+
capability: "indexeddb"
|
|
1606
|
+
};
|
|
1607
|
+
const storage = availableLocalStorage();
|
|
1608
|
+
if (storage !== void 0 && storage !== null)
|
|
1609
|
+
return { storage: new LocalStorageQueueStorage(storage), capability: "localstorage" };
|
|
1546
1610
|
} catch {
|
|
1547
1611
|
}
|
|
1548
|
-
return new MemoryQueueStorage();
|
|
1612
|
+
return { storage: new MemoryQueueStorage(), capability: "memory" };
|
|
1549
1613
|
}
|
|
1550
|
-
function
|
|
1551
|
-
|
|
1614
|
+
function offlineOperationId(operation) {
|
|
1615
|
+
const identity = operation.request.userId ?? operation.request.anonymousSessionId ?? "anonymous";
|
|
1616
|
+
return operation.kind === "checkIn" ? `checkIn:${operation.request.rallyId}:${identity}:${operation.request.idempotencyKey}` : `claimReward:${operation.request.rallyId}:${identity}:${operation.request.idempotencyKey}`;
|
|
1552
1617
|
}
|
|
1553
1618
|
function requestScope(operation) {
|
|
1554
1619
|
return {
|
|
@@ -1578,12 +1643,14 @@ function randomId() {
|
|
|
1578
1643
|
}
|
|
1579
1644
|
var OfflineQueue = class {
|
|
1580
1645
|
#storage;
|
|
1646
|
+
#queueCapability;
|
|
1581
1647
|
#configuredKey;
|
|
1582
1648
|
#rallyId;
|
|
1583
1649
|
#userId;
|
|
1584
1650
|
#conflictPolicy;
|
|
1585
1651
|
#onSyncConflict;
|
|
1586
1652
|
#operations = [];
|
|
1653
|
+
#rejectedHistory = [];
|
|
1587
1654
|
#loaded = false;
|
|
1588
1655
|
#state = "idle";
|
|
1589
1656
|
#error = null;
|
|
@@ -1594,13 +1661,22 @@ var OfflineQueue = class {
|
|
|
1594
1661
|
#retryOptions;
|
|
1595
1662
|
#instanceId = randomId();
|
|
1596
1663
|
#lockStorage;
|
|
1664
|
+
#observedLocks = /* @__PURE__ */ new Map();
|
|
1665
|
+
#warnedMemoryLock = false;
|
|
1597
1666
|
#storageListener;
|
|
1598
1667
|
#channel = null;
|
|
1599
1668
|
constructor(options = {}) {
|
|
1600
|
-
if (options.storage !== void 0)
|
|
1601
|
-
|
|
1669
|
+
if (options.storage !== void 0) {
|
|
1670
|
+
this.#storage = options.storage;
|
|
1671
|
+
this.#queueCapability = "custom";
|
|
1672
|
+
} else if (options.storageLike !== void 0 && options.storageLike !== null) {
|
|
1602
1673
|
this.#storage = new LocalStorageQueueStorage(options.storageLike);
|
|
1603
|
-
|
|
1674
|
+
this.#queueCapability = "localstorage";
|
|
1675
|
+
} else {
|
|
1676
|
+
const selected = defaultStorage(options.databaseName);
|
|
1677
|
+
this.#storage = selected.storage;
|
|
1678
|
+
this.#queueCapability = selected.capability;
|
|
1679
|
+
}
|
|
1604
1680
|
this.#configuredKey = options.key;
|
|
1605
1681
|
this.#rallyId = options.rallyId;
|
|
1606
1682
|
this.#userId = options.userId ?? null;
|
|
@@ -1616,7 +1692,7 @@ var OfflineQueue = class {
|
|
|
1616
1692
|
initialIntervalMs: Math.max(0, retryOptions.initialIntervalMs),
|
|
1617
1693
|
backoffMultiplier: Math.max(1, retryOptions.backoffMultiplier)
|
|
1618
1694
|
};
|
|
1619
|
-
this.#lockStorage = options.storageLike ??
|
|
1695
|
+
this.#lockStorage = options.storageLike ?? availableLocalStorage();
|
|
1620
1696
|
if (this.#synchronizeInstances) this.#subscribeToExternalChanges();
|
|
1621
1697
|
}
|
|
1622
1698
|
get syncState() {
|
|
@@ -1625,6 +1701,12 @@ var OfflineQueue = class {
|
|
|
1625
1701
|
get pendingCount() {
|
|
1626
1702
|
return this.#operations.length;
|
|
1627
1703
|
}
|
|
1704
|
+
get queueCapability() {
|
|
1705
|
+
return this.#queueCapability;
|
|
1706
|
+
}
|
|
1707
|
+
get rejectedHistory() {
|
|
1708
|
+
return this.#rejectedHistory;
|
|
1709
|
+
}
|
|
1628
1710
|
get error() {
|
|
1629
1711
|
return this.#error;
|
|
1630
1712
|
}
|
|
@@ -1648,7 +1730,20 @@ var OfflineQueue = class {
|
|
|
1648
1730
|
}
|
|
1649
1731
|
async initialize() {
|
|
1650
1732
|
if (this.#loaded) return;
|
|
1651
|
-
|
|
1733
|
+
try {
|
|
1734
|
+
const key = this.#storageKey();
|
|
1735
|
+
this.#operations = (await this.#storage.load(key)).map(normalizeOperation);
|
|
1736
|
+
this.#rejectedHistory = (await this.#storage.loadRejectedHistory?.(key))?.map(normalizeRejectedHistory) ?? [];
|
|
1737
|
+
} catch (error) {
|
|
1738
|
+
if (this.#queueCapability === "memory") throw error;
|
|
1739
|
+
this.#storage = new MemoryQueueStorage();
|
|
1740
|
+
this.#queueCapability = "memory";
|
|
1741
|
+
this.#operations = [];
|
|
1742
|
+
this.#rejectedHistory = [];
|
|
1743
|
+
this.#warnMemoryLock(
|
|
1744
|
+
`Offline queue persistence is unavailable; queued data can be lost after reload (${String(error)}).`
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1652
1747
|
this.#loaded = true;
|
|
1653
1748
|
}
|
|
1654
1749
|
/** Releases browser listeners when the queue is no longer used. */
|
|
@@ -1691,8 +1786,8 @@ var OfflineQueue = class {
|
|
|
1691
1786
|
throw new Error("Offline operation belongs to another rally or user queue.");
|
|
1692
1787
|
}
|
|
1693
1788
|
await this.initialize();
|
|
1694
|
-
const id2 =
|
|
1695
|
-
if (this.#operations.some((item) =>
|
|
1789
|
+
const id2 = offlineOperationId(operation);
|
|
1790
|
+
if (this.#operations.some((item) => offlineOperationId(item) === id2)) return;
|
|
1696
1791
|
this.#operations = [...this.#operations, { ...operation, status: "PENDING", attempts: 0 }];
|
|
1697
1792
|
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1698
1793
|
this.#announceChange();
|
|
@@ -1709,6 +1804,47 @@ var OfflineQueue = class {
|
|
|
1709
1804
|
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1710
1805
|
this.#announceChange();
|
|
1711
1806
|
}
|
|
1807
|
+
async discardRejected(operationId) {
|
|
1808
|
+
await this.initialize();
|
|
1809
|
+
const next = this.#rejectedHistory.filter(
|
|
1810
|
+
(entry) => offlineOperationId(entry.operation) !== operationId
|
|
1811
|
+
);
|
|
1812
|
+
if (next.length === this.#rejectedHistory.length) return false;
|
|
1813
|
+
this.#rejectedHistory = next;
|
|
1814
|
+
await this.#saveRejectedHistory();
|
|
1815
|
+
this.#announceChange();
|
|
1816
|
+
return true;
|
|
1817
|
+
}
|
|
1818
|
+
async retryRejected(operationId) {
|
|
1819
|
+
await this.initialize();
|
|
1820
|
+
const entry = this.#rejectedHistory.find(
|
|
1821
|
+
(candidate) => offlineOperationId(candidate.operation) === operationId
|
|
1822
|
+
);
|
|
1823
|
+
if (entry === void 0) return false;
|
|
1824
|
+
if (!this.#operations.some((operation) => offlineOperationId(operation) === operationId))
|
|
1825
|
+
this.#operations = [
|
|
1826
|
+
...this.#operations,
|
|
1827
|
+
{ ...entry.operation, status: "PENDING", attempts: 0 }
|
|
1828
|
+
];
|
|
1829
|
+
this.#rejectedHistory = this.#rejectedHistory.filter((candidate) => candidate !== entry);
|
|
1830
|
+
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1831
|
+
await this.#saveRejectedHistory();
|
|
1832
|
+
this.#announceChange();
|
|
1833
|
+
return true;
|
|
1834
|
+
}
|
|
1835
|
+
async discardRejectedOperation(operationId) {
|
|
1836
|
+
return this.discardRejected(operationId);
|
|
1837
|
+
}
|
|
1838
|
+
async retryRejectedOperation(operationId) {
|
|
1839
|
+
return this.retryRejected(operationId);
|
|
1840
|
+
}
|
|
1841
|
+
async clearRejectedHistory() {
|
|
1842
|
+
await this.initialize();
|
|
1843
|
+
if (this.#rejectedHistory.length === 0) return;
|
|
1844
|
+
this.#rejectedHistory = [];
|
|
1845
|
+
await this.#saveRejectedHistory();
|
|
1846
|
+
this.#announceChange();
|
|
1847
|
+
}
|
|
1712
1848
|
async sync(sender = this.#sender) {
|
|
1713
1849
|
await this.initialize();
|
|
1714
1850
|
if (sender === void 0) throw new Error("OfflineQueue.sync requires a sender.");
|
|
@@ -1724,7 +1860,7 @@ var OfflineQueue = class {
|
|
|
1724
1860
|
}
|
|
1725
1861
|
async #run(sender) {
|
|
1726
1862
|
const locks = globalThis.navigator?.locks;
|
|
1727
|
-
if (locks !== void 0) {
|
|
1863
|
+
if (locks !== void 0 && typeof locks.request === "function") {
|
|
1728
1864
|
let callbackStarted = false;
|
|
1729
1865
|
try {
|
|
1730
1866
|
const acquired = await locks.request(
|
|
@@ -1747,6 +1883,10 @@ var OfflineQueue = class {
|
|
|
1747
1883
|
if (callbackStarted) throw error;
|
|
1748
1884
|
}
|
|
1749
1885
|
}
|
|
1886
|
+
if (this.#lockStorage === null)
|
|
1887
|
+
this.#warnMemoryLock(
|
|
1888
|
+
"No cross-tab storage lock is available; offline sync is single-tab only."
|
|
1889
|
+
);
|
|
1750
1890
|
await this.#runWithStorageLock(sender);
|
|
1751
1891
|
}
|
|
1752
1892
|
async #runWithStorageLock(sender) {
|
|
@@ -1777,20 +1917,38 @@ var OfflineQueue = class {
|
|
|
1777
1917
|
const error2 = errorValue(response.error ?? response.reason, "RETRYABLE_ERROR");
|
|
1778
1918
|
await this.#updateOperationStatus("PENDING", attempt + 1);
|
|
1779
1919
|
await this.#syncResultListener?.({ operation, status: response.status, error: error2 });
|
|
1780
|
-
if (attempt >= this.#retryOptions.maxRetries)
|
|
1920
|
+
if (attempt >= this.#retryOptions.maxRetries) {
|
|
1921
|
+
await this.#updateOperationStatus("FAILED_RETRYABLE", attempt + 1);
|
|
1922
|
+
throw new Error(error2.message);
|
|
1923
|
+
}
|
|
1781
1924
|
const interval = this.#retryOptions.initialIntervalMs * this.#retryOptions.backoffMultiplier ** attempt;
|
|
1782
1925
|
await new Promise((resolve) => setTimeout(resolve, Math.max(0, interval)));
|
|
1783
1926
|
attempt += 1;
|
|
1784
1927
|
}
|
|
1785
1928
|
const result = response.result;
|
|
1786
|
-
const state = response.state ?? (result !== void 0 && "conflict" in result && result.conflict === true ?
|
|
1787
|
-
const error = response.status === "REJECTED_PERMANENT" ? errorValue(
|
|
1929
|
+
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);
|
|
1930
|
+
const error = response.status === "REJECTED_PERMANENT" ? errorValue(
|
|
1931
|
+
response.error ?? response.reason ?? (result !== void 0 && "ok" in result && !result.ok ? result.error : void 0),
|
|
1932
|
+
"REJECTED_PERMANENT"
|
|
1933
|
+
) : void 0;
|
|
1788
1934
|
await this.#updateOperationStatus(
|
|
1789
|
-
response.status === "ACCEPTED" ? "ACCEPTED" : "
|
|
1935
|
+
response.status === "ACCEPTED" ? "ACCEPTED" : "REJECTED_PERMANENT",
|
|
1790
1936
|
attempt + 1
|
|
1791
1937
|
);
|
|
1792
|
-
const
|
|
1793
|
-
|
|
1938
|
+
const eventState = state;
|
|
1939
|
+
if (response.status === "REJECTED_PERMANENT" && error !== void 0) {
|
|
1940
|
+
this.#rejectedHistory = [
|
|
1941
|
+
...this.#rejectedHistory,
|
|
1942
|
+
{
|
|
1943
|
+
operation: { ...operation, status: "REJECTED_PERMANENT", attempts: attempt + 1 },
|
|
1944
|
+
reason: error,
|
|
1945
|
+
errorCode: error.code,
|
|
1946
|
+
rejectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1947
|
+
attempts: attempt + 1
|
|
1948
|
+
}
|
|
1949
|
+
];
|
|
1950
|
+
await this.#saveRejectedHistory();
|
|
1951
|
+
}
|
|
1794
1952
|
this.#operations = this.#operations.slice(1);
|
|
1795
1953
|
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1796
1954
|
this.#announceChange();
|
|
@@ -1822,7 +1980,8 @@ var OfflineQueue = class {
|
|
|
1822
1980
|
const windowLike = globalThis.window;
|
|
1823
1981
|
if (windowLike !== void 0) {
|
|
1824
1982
|
this.#storageListener = (event) => {
|
|
1825
|
-
if (event.key === this.#storageKey()
|
|
1983
|
+
if (event.key === this.#storageKey() || event.key === `${this.#storageKey()}:rejected-history`)
|
|
1984
|
+
void this.#reloadFromStorage();
|
|
1826
1985
|
};
|
|
1827
1986
|
windowLike.addEventListener("storage", this.#storageListener);
|
|
1828
1987
|
}
|
|
@@ -1831,6 +1990,20 @@ var OfflineQueue = class {
|
|
|
1831
1990
|
try {
|
|
1832
1991
|
this.#channel = new Channel("stamprally:queue-sync");
|
|
1833
1992
|
this.#channel.addEventListener("message", (event) => {
|
|
1993
|
+
if (typeof event.data === "object" && event.data !== null) {
|
|
1994
|
+
const data = event.data;
|
|
1995
|
+
if (data.type === "lock" && data.lockKey === this.#lockKey()) {
|
|
1996
|
+
this.#observedLocks.set(data.lockKey, {
|
|
1997
|
+
owner: typeof data.owner === "string" ? data.owner : "unknown",
|
|
1998
|
+
expiresAt: typeof data.expiresAt === "number" ? data.expiresAt : 0
|
|
1999
|
+
});
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
if (data.type === "unlock" && data.lockKey === this.#lockKey()) {
|
|
2003
|
+
this.#observedLocks.delete(data.lockKey);
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
1834
2007
|
if (typeof event.data === "object" && event.data !== null && "key" in event.data && event.data.key === this.#storageKey())
|
|
1835
2008
|
void this.#reloadFromStorage();
|
|
1836
2009
|
});
|
|
@@ -1840,12 +2013,18 @@ var OfflineQueue = class {
|
|
|
1840
2013
|
}
|
|
1841
2014
|
}
|
|
1842
2015
|
#announceChange() {
|
|
1843
|
-
this.#channel?.postMessage({
|
|
2016
|
+
this.#channel?.postMessage({
|
|
2017
|
+
type: "change",
|
|
2018
|
+
key: this.#storageKey(),
|
|
2019
|
+
owner: this.#instanceId
|
|
2020
|
+
});
|
|
1844
2021
|
}
|
|
1845
2022
|
async #reloadFromStorage() {
|
|
1846
2023
|
if (this.#state === "syncing") return;
|
|
1847
2024
|
try {
|
|
1848
|
-
|
|
2025
|
+
const key = this.#storageKey();
|
|
2026
|
+
this.#operations = (await this.#storage.load(key)).map(normalizeOperation);
|
|
2027
|
+
this.#rejectedHistory = (await this.#storage.loadRejectedHistory?.(key))?.map(normalizeRejectedHistory) ?? this.#rejectedHistory;
|
|
1849
2028
|
this.#loaded = true;
|
|
1850
2029
|
} catch {
|
|
1851
2030
|
}
|
|
@@ -1859,6 +2038,9 @@ var OfflineQueue = class {
|
|
|
1859
2038
|
const local = syncLocks.get(key);
|
|
1860
2039
|
if (local !== void 0 && local.expiresAt > now && local.owner !== this.#instanceId)
|
|
1861
2040
|
return false;
|
|
2041
|
+
const observed = this.#observedLocks.get(key);
|
|
2042
|
+
if (observed !== void 0 && observed.expiresAt > now && observed.owner !== this.#instanceId)
|
|
2043
|
+
return false;
|
|
1862
2044
|
if (this.#lockStorage !== null) {
|
|
1863
2045
|
try {
|
|
1864
2046
|
const existing = this.#lockStorage.getItem(key);
|
|
@@ -1871,7 +2053,14 @@ var OfflineQueue = class {
|
|
|
1871
2053
|
key,
|
|
1872
2054
|
JSON.stringify({ owner: this.#instanceId, expiresAt: now + SYNC_LOCK_TTL_MS })
|
|
1873
2055
|
);
|
|
2056
|
+
this.#channel?.postMessage({
|
|
2057
|
+
type: "lock",
|
|
2058
|
+
lockKey: key,
|
|
2059
|
+
owner: this.#instanceId,
|
|
2060
|
+
expiresAt: now + SYNC_LOCK_TTL_MS
|
|
2061
|
+
});
|
|
1874
2062
|
} catch {
|
|
2063
|
+
this.#warnMemoryLock("Persistent lock access failed; offline sync is single-tab only.");
|
|
1875
2064
|
}
|
|
1876
2065
|
}
|
|
1877
2066
|
syncLocks.set(key, { owner: this.#instanceId, expiresAt: now + SYNC_LOCK_TTL_MS });
|
|
@@ -1886,6 +2075,7 @@ var OfflineQueue = class {
|
|
|
1886
2075
|
const value = this.#lockStorage.getItem(key);
|
|
1887
2076
|
if (value !== null && JSON.parse(value).owner === this.#instanceId)
|
|
1888
2077
|
this.#lockStorage.removeItem?.(key);
|
|
2078
|
+
this.#channel?.postMessage({ type: "unlock", lockKey: key, owner: this.#instanceId });
|
|
1889
2079
|
} catch {
|
|
1890
2080
|
}
|
|
1891
2081
|
}
|
|
@@ -1914,14 +2104,33 @@ var OfflineQueue = class {
|
|
|
1914
2104
|
const policy = (typeof configured === "function" ? await configured({ operation, localState, serverState }) : configured) ?? this.#conflictPolicy;
|
|
1915
2105
|
return resolveRallyStateConflict(serverState, localState, { policy });
|
|
1916
2106
|
}
|
|
2107
|
+
async #saveRejectedHistory() {
|
|
2108
|
+
await this.#storage.saveRejectedHistory?.(this.#storageKey(), this.#rejectedHistory);
|
|
2109
|
+
}
|
|
2110
|
+
#warnMemoryLock(message) {
|
|
2111
|
+
if (this.#warnedMemoryLock) return;
|
|
2112
|
+
this.#warnedMemoryLock = true;
|
|
2113
|
+
console.warn(`[@stamprally/core] ${message}`);
|
|
2114
|
+
}
|
|
1917
2115
|
};
|
|
1918
2116
|
function normalizeOperation(operation) {
|
|
2117
|
+
const status = operation.status;
|
|
1919
2118
|
return {
|
|
1920
2119
|
...operation,
|
|
1921
|
-
status:
|
|
2120
|
+
status: status === "IN_FLIGHT" || status === "REJECTED" ? "PENDING" : status === "RETRYABLE_ERROR" ? "FAILED_RETRYABLE" : operation.status ?? "PENDING",
|
|
1922
2121
|
attempts: operation.attempts ?? 0
|
|
1923
2122
|
};
|
|
1924
2123
|
}
|
|
2124
|
+
function normalizeRejectedHistory(entry) {
|
|
2125
|
+
return {
|
|
2126
|
+
...entry,
|
|
2127
|
+
operation: normalizeOperation(entry.operation),
|
|
2128
|
+
reason: errorValue(entry.reason, "REJECTED_PERMANENT"),
|
|
2129
|
+
errorCode: entry.errorCode || entry.reason.code,
|
|
2130
|
+
rejectedAt: entry.rejectedAt || (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2131
|
+
attempts: entry.attempts ?? entry.operation.attempts ?? 0
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
1925
2134
|
|
|
1926
2135
|
// src/crypto/token.ts
|
|
1927
2136
|
var encoder = new TextEncoder();
|
|
@@ -2938,6 +3147,7 @@ exports.isRewardState = isRewardState;
|
|
|
2938
3147
|
exports.isStampRallyState = isStampRallyState;
|
|
2939
3148
|
exports.issueClaimTicketNumber = issueClaimTicketNumber;
|
|
2940
3149
|
exports.normalizePasscode = normalizePasscode;
|
|
3150
|
+
exports.offlineOperationId = offlineOperationId;
|
|
2941
3151
|
exports.parseAdminConfig = parseAdminConfig;
|
|
2942
3152
|
exports.parsePublicConfig = parsePublicConfig;
|
|
2943
3153
|
exports.processStamp = processStamp;
|