@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 CHANGED
@@ -1,4 +1,4 @@
1
- # @stamprally/core v0.15.0
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.15.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 new IndexedDBOfflineQueueStorage({
1541
- indexedDB,
1542
- ...databaseName === void 0 ? {} : { databaseName }
1543
- });
1544
- const storage = globalThis.localStorage;
1545
- if (storage !== void 0 && storage !== null) return new LocalStorageQueueStorage(storage);
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 operationId(operation) {
1551
- return operation.kind === "checkIn" ? `checkIn:${operation.request.rallyId}:${operation.request.userId ?? "anonymous"}:${operation.request.idempotencyKey}` : `claimReward:${operation.request.rallyId}:${operation.request.userId ?? "anonymous"}:${operation.request.idempotencyKey}`;
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) this.#storage = options.storage;
1601
- else if (options.storageLike !== void 0 && options.storageLike !== null)
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
- else this.#storage = defaultStorage(options.databaseName);
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 ?? globalThis.localStorage ?? null;
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
- this.#operations = (await this.#storage.load(this.#storageKey())).map(normalizeOperation);
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 = operationId(operation);
1695
- if (this.#operations.some((item) => operationId(item) === id2)) return;
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) throw new Error(error2.message);
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 ? await this.resolveConflict(operation, result.localState, result.serverState) : result !== void 0 && "ok" in result && result.ok ? result.value.state : void 0);
1787
- const error = response.status === "REJECTED_PERMANENT" ? errorValue(response.error ?? response.reason, "REJECTED_PERMANENT") : void 0;
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" : "REJECTED",
1935
+ response.status === "ACCEPTED" ? "ACCEPTED" : "REJECTED_PERMANENT",
1790
1936
  attempt + 1
1791
1937
  );
1792
- const fallbackState = response.status === "REJECTED_PERMANENT" ? operation.request.state : void 0;
1793
- const eventState = state ?? fallbackState;
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()) void this.#reloadFromStorage();
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({ key: this.#storageKey(), owner: this.#instanceId });
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
- this.#operations = (await this.#storage.load(this.#storageKey())).map(normalizeOperation);
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: operation.status === "IN_FLIGHT" ? "PENDING" : operation.status ?? "PENDING",
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;