@stamprally/core 0.13.0 → 0.15.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 +7 -3
- package/dist/index.cjs +344 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -3
- package/dist/index.d.ts +40 -3
- package/dist/index.js +343 -31
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -59,7 +59,9 @@ function calculateProgress(state, config) {
|
|
|
59
59
|
const acquired = new Set(
|
|
60
60
|
state.records.map((record) => record.stampId).filter((id2) => ids.has(id2))
|
|
61
61
|
);
|
|
62
|
-
const remaining = config.spots.filter(
|
|
62
|
+
const remaining = config.spots.filter(
|
|
63
|
+
(spot2) => !acquired.has(spot2.id) && (spot2.prerequisites === void 0 || spot2.prerequisites.every((id2) => acquired.has(id2)))
|
|
64
|
+
);
|
|
63
65
|
return {
|
|
64
66
|
acquired: acquired.size,
|
|
65
67
|
total: config.spots.length,
|
|
@@ -613,7 +615,13 @@ function cloneState(state) {
|
|
|
613
615
|
return {
|
|
614
616
|
...state,
|
|
615
617
|
records: state.records.map(cloneRecord),
|
|
616
|
-
...state.rewards === void 0 ? {} : { rewards: state.rewards.map(cloneRewardState) }
|
|
618
|
+
...state.rewards === void 0 ? {} : { rewards: state.rewards.map(cloneRewardState) },
|
|
619
|
+
...state.inventory === void 0 ? {} : {
|
|
620
|
+
inventory: {
|
|
621
|
+
...state.inventory,
|
|
622
|
+
...state.inventory.rewardRemaining === void 0 ? {} : { rewardRemaining: { ...state.inventory.rewardRemaining } }
|
|
623
|
+
}
|
|
624
|
+
}
|
|
617
625
|
};
|
|
618
626
|
}
|
|
619
627
|
function isRecord(value) {
|
|
@@ -633,7 +641,7 @@ function isRewardState(value) {
|
|
|
633
641
|
function isStampRallyState(value) {
|
|
634
642
|
if (typeof value !== "object" || value === null) return false;
|
|
635
643
|
const state = value;
|
|
636
|
-
return typeof state.rallyId === "string" && (typeof state.userId === "string" || state.userId === null) && typeof state.updatedAt === "string" && Array.isArray(state.records) && state.records.every(isRecord) && (state.rewards === void 0 || Array.isArray(state.rewards) && state.rewards.every(isRewardState));
|
|
644
|
+
return typeof state.rallyId === "string" && (typeof state.userId === "string" || state.userId === null) && typeof state.updatedAt === "string" && Array.isArray(state.records) && state.records.every(isRecord) && (state.rewards === void 0 || Array.isArray(state.rewards) && state.rewards.every(isRewardState)) && (state.inventory === void 0 || typeof state.inventory === "object" && state.inventory !== null && !Array.isArray(state.inventory) && (state.inventory.sharedRemaining === void 0 || typeof state.inventory.sharedRemaining === "number" && Number.isInteger(state.inventory.sharedRemaining) && state.inventory.sharedRemaining >= 0) && (state.inventory.rewardRemaining === void 0 || typeof state.inventory.rewardRemaining === "object" && state.inventory.rewardRemaining !== null && !Array.isArray(state.inventory.rewardRemaining)));
|
|
637
645
|
}
|
|
638
646
|
function isValidDate(value) {
|
|
639
647
|
return typeof value === "string" && !Number.isNaN(Date.parse(value));
|
|
@@ -678,6 +686,35 @@ var InMemoryStorage = class {
|
|
|
678
686
|
function storageKey(rallyId, userId) {
|
|
679
687
|
return `stamprally:${rallyId}:${userId ?? "anonymous"}`;
|
|
680
688
|
}
|
|
689
|
+
function createAnonymousSessionId(storage) {
|
|
690
|
+
const key = "stamprally:anonymous-session-id";
|
|
691
|
+
try {
|
|
692
|
+
const browserStorage = typeof window === "undefined" ? null : window.localStorage;
|
|
693
|
+
const value = storage?.getItem(key) ?? browserStorage?.getItem(key);
|
|
694
|
+
if (value !== null && value !== void 0 && isUuidV4(value)) return value;
|
|
695
|
+
const generated = randomUuidV4();
|
|
696
|
+
(storage ?? browserStorage)?.setItem(key, generated);
|
|
697
|
+
return generated;
|
|
698
|
+
} catch {
|
|
699
|
+
return randomUuidV4();
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
function isUuidV4(value) {
|
|
703
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
704
|
+
}
|
|
705
|
+
function randomUuidV4() {
|
|
706
|
+
const cryptoApi3 = globalThis.crypto;
|
|
707
|
+
if (cryptoApi3?.randomUUID !== void 0) return cryptoApi3.randomUUID();
|
|
708
|
+
if (cryptoApi3?.getRandomValues !== void 0) {
|
|
709
|
+
const bytes2 = cryptoApi3.getRandomValues(new Uint8Array(16));
|
|
710
|
+
bytes2[6] = (bytes2[6] ?? 0) & 15 | 64;
|
|
711
|
+
bytes2[8] = (bytes2[8] ?? 0) & 63 | 128;
|
|
712
|
+
const hex = Array.from(bytes2, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
713
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
714
|
+
}
|
|
715
|
+
const random = `${Date.now().toString(16)}${Math.random().toString(16).slice(2)}`.padEnd(32, "0").slice(0, 32);
|
|
716
|
+
return `${random.slice(0, 8)}-${random.slice(8, 12)}-4${random.slice(13, 16)}-8${random.slice(17, 20)}-${random.slice(20)}`;
|
|
717
|
+
}
|
|
681
718
|
var defaultStorageWarningHandler = (error) => {
|
|
682
719
|
console.warn(`[@stamprally/core] ${error.message}`, error);
|
|
683
720
|
};
|
|
@@ -1067,6 +1104,7 @@ var StampRallyClient = class {
|
|
|
1067
1104
|
#config;
|
|
1068
1105
|
#offlineQueue;
|
|
1069
1106
|
#userId;
|
|
1107
|
+
#anonymousSessionId;
|
|
1070
1108
|
#state = null;
|
|
1071
1109
|
#initialization = null;
|
|
1072
1110
|
#queue = Promise.resolve();
|
|
@@ -1074,7 +1112,8 @@ var StampRallyClient = class {
|
|
|
1074
1112
|
this.#config = config;
|
|
1075
1113
|
this.#options = isStorage(storageOrOptions) ? { storage: storageOrOptions } : storageOrOptions;
|
|
1076
1114
|
this.#storage = this.#options.storage ?? new InMemoryStorage();
|
|
1077
|
-
this.#
|
|
1115
|
+
this.#anonymousSessionId = this.#options.anonymousSessionId ?? createAnonymousSessionId();
|
|
1116
|
+
this.#userId = this.#options.userId ?? this.#anonymousSessionId;
|
|
1078
1117
|
this.#offlineQueue = this.#options.offlineQueue;
|
|
1079
1118
|
this.#offlineQueue?.setSyncResultListener((event) => this.#handleOfflineSyncResult(event));
|
|
1080
1119
|
}
|
|
@@ -1087,6 +1126,9 @@ var StampRallyClient = class {
|
|
|
1087
1126
|
getUserId() {
|
|
1088
1127
|
return this.#userId;
|
|
1089
1128
|
}
|
|
1129
|
+
getAnonymousSessionId() {
|
|
1130
|
+
return this.#anonymousSessionId;
|
|
1131
|
+
}
|
|
1090
1132
|
get syncState() {
|
|
1091
1133
|
return this.#offlineQueue?.syncState ?? "idle";
|
|
1092
1134
|
}
|
|
@@ -1124,11 +1166,12 @@ var StampRallyClient = class {
|
|
|
1124
1166
|
}
|
|
1125
1167
|
switchUser(newUserId) {
|
|
1126
1168
|
return this.#enqueue(async () => {
|
|
1127
|
-
|
|
1128
|
-
this.#userId
|
|
1169
|
+
const nextUserId = newUserId ?? this.#anonymousSessionId;
|
|
1170
|
+
if (this.#userId === nextUserId && this.#state !== null) return this.#state;
|
|
1171
|
+
this.#userId = nextUserId;
|
|
1129
1172
|
this.#state = null;
|
|
1130
1173
|
this.#initialization = null;
|
|
1131
|
-
await this.#offlineQueue?.switchUser(
|
|
1174
|
+
await this.#offlineQueue?.switchUser(nextUserId);
|
|
1132
1175
|
return this.initialize();
|
|
1133
1176
|
});
|
|
1134
1177
|
}
|
|
@@ -1196,7 +1239,8 @@ var StampRallyClient = class {
|
|
|
1196
1239
|
proofData,
|
|
1197
1240
|
idempotencyKey: options.idempotencyKey ?? id("check-in"),
|
|
1198
1241
|
now,
|
|
1199
|
-
state: current
|
|
1242
|
+
state: current,
|
|
1243
|
+
...this.#userId === this.#anonymousSessionId ? { anonymousSessionId: this.#anonymousSessionId } : {}
|
|
1200
1244
|
};
|
|
1201
1245
|
const remote = this.#options.syncAdapter?.checkIn;
|
|
1202
1246
|
if (options.sync !== false && remote !== void 0) {
|
|
@@ -1252,7 +1296,8 @@ var StampRallyClient = class {
|
|
|
1252
1296
|
idempotencyKey: options.idempotencyKey ?? id("claim"),
|
|
1253
1297
|
now,
|
|
1254
1298
|
options,
|
|
1255
|
-
state: current
|
|
1299
|
+
state: current,
|
|
1300
|
+
...this.#userId === this.#anonymousSessionId ? { anonymousSessionId: this.#anonymousSessionId } : {}
|
|
1256
1301
|
};
|
|
1257
1302
|
const remote = this.#options.syncAdapter?.claimReward;
|
|
1258
1303
|
if (options.sync !== false && remote !== void 0) {
|
|
@@ -1304,13 +1349,10 @@ var StampRallyClient = class {
|
|
|
1304
1349
|
const serverState = await adapter.sync({
|
|
1305
1350
|
rallyId: this.#config.id,
|
|
1306
1351
|
userId: this.#userId,
|
|
1307
|
-
state: this.#state ?? current
|
|
1308
|
-
|
|
1309
|
-
const localState = this.#state ?? current;
|
|
1310
|
-
const merged = this.#offlineQueue === void 0 ? serverState : resolveRallyStateConflict(serverState, localState, {
|
|
1311
|
-
policy: this.#offlineQueue.conflictPolicy
|
|
1352
|
+
state: this.#state ?? current,
|
|
1353
|
+
...this.#userId === this.#anonymousSessionId ? { anonymousSessionId: this.#anonymousSessionId } : {}
|
|
1312
1354
|
});
|
|
1313
|
-
const next = this.#reconcile(
|
|
1355
|
+
const next = this.#reconcile(serverState);
|
|
1314
1356
|
await this.#storage.save(next);
|
|
1315
1357
|
this.#state = next;
|
|
1316
1358
|
this.#emit(next);
|
|
@@ -1522,6 +1564,16 @@ function errorValue(value, fallbackCode) {
|
|
|
1522
1564
|
if (typeof value === "string") return { code: fallbackCode, message: value };
|
|
1523
1565
|
return { code: fallbackCode, message: "Offline operation was rejected." };
|
|
1524
1566
|
}
|
|
1567
|
+
var syncLocks = /* @__PURE__ */ new Map();
|
|
1568
|
+
var SYNC_LOCK_TTL_MS = 3e4;
|
|
1569
|
+
var DEFAULT_RETRY_OPTIONS = {
|
|
1570
|
+
maxRetries: 0,
|
|
1571
|
+
initialIntervalMs: 250,
|
|
1572
|
+
backoffMultiplier: 2
|
|
1573
|
+
};
|
|
1574
|
+
function randomId() {
|
|
1575
|
+
return globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1576
|
+
}
|
|
1525
1577
|
var OfflineQueue = class {
|
|
1526
1578
|
#storage;
|
|
1527
1579
|
#configuredKey;
|
|
@@ -1536,6 +1588,12 @@ var OfflineQueue = class {
|
|
|
1536
1588
|
#sender;
|
|
1537
1589
|
#syncPromise = null;
|
|
1538
1590
|
#syncResultListener;
|
|
1591
|
+
#synchronizeInstances;
|
|
1592
|
+
#retryOptions;
|
|
1593
|
+
#instanceId = randomId();
|
|
1594
|
+
#lockStorage;
|
|
1595
|
+
#storageListener;
|
|
1596
|
+
#channel = null;
|
|
1539
1597
|
constructor(options = {}) {
|
|
1540
1598
|
if (options.storage !== void 0) this.#storage = options.storage;
|
|
1541
1599
|
else if (options.storageLike !== void 0 && options.storageLike !== null)
|
|
@@ -1546,6 +1604,18 @@ var OfflineQueue = class {
|
|
|
1546
1604
|
this.#userId = options.userId ?? null;
|
|
1547
1605
|
this.#conflictPolicy = options.conflictPolicy ?? "server_wins";
|
|
1548
1606
|
this.#onSyncConflict = options.onSyncConflict;
|
|
1607
|
+
this.#synchronizeInstances = options.synchronizeInstances ?? true;
|
|
1608
|
+
const retryOptions = {
|
|
1609
|
+
...DEFAULT_RETRY_OPTIONS,
|
|
1610
|
+
...options.retryOptions ?? options.syncRetryOptions ?? options.retry ?? {}
|
|
1611
|
+
};
|
|
1612
|
+
this.#retryOptions = {
|
|
1613
|
+
maxRetries: Math.max(0, Math.floor(retryOptions.maxRetries)),
|
|
1614
|
+
initialIntervalMs: Math.max(0, retryOptions.initialIntervalMs),
|
|
1615
|
+
backoffMultiplier: Math.max(1, retryOptions.backoffMultiplier)
|
|
1616
|
+
};
|
|
1617
|
+
this.#lockStorage = options.storageLike ?? globalThis.localStorage ?? null;
|
|
1618
|
+
if (this.#synchronizeInstances) this.#subscribeToExternalChanges();
|
|
1549
1619
|
}
|
|
1550
1620
|
get syncState() {
|
|
1551
1621
|
return this.#state;
|
|
@@ -1576,9 +1646,19 @@ var OfflineQueue = class {
|
|
|
1576
1646
|
}
|
|
1577
1647
|
async initialize() {
|
|
1578
1648
|
if (this.#loaded) return;
|
|
1579
|
-
this.#operations =
|
|
1649
|
+
this.#operations = (await this.#storage.load(this.#storageKey())).map(normalizeOperation);
|
|
1580
1650
|
this.#loaded = true;
|
|
1581
1651
|
}
|
|
1652
|
+
/** Releases browser listeners when the queue is no longer used. */
|
|
1653
|
+
dispose() {
|
|
1654
|
+
const windowLike = globalThis.window;
|
|
1655
|
+
if (windowLike !== void 0 && this.#storageListener !== void 0)
|
|
1656
|
+
windowLike.removeEventListener("storage", this.#storageListener);
|
|
1657
|
+
this.#storageListener = void 0;
|
|
1658
|
+
this.#channel?.close();
|
|
1659
|
+
this.#channel = null;
|
|
1660
|
+
this.#releaseSyncLock();
|
|
1661
|
+
}
|
|
1582
1662
|
/** Selects a rally/user queue scope and loads its pending operations. */
|
|
1583
1663
|
async setScope(rallyId, userId) {
|
|
1584
1664
|
if (this.#configuredKey !== void 0) {
|
|
@@ -1611,8 +1691,9 @@ var OfflineQueue = class {
|
|
|
1611
1691
|
await this.initialize();
|
|
1612
1692
|
const id2 = operationId(operation);
|
|
1613
1693
|
if (this.#operations.some((item) => operationId(item) === id2)) return;
|
|
1614
|
-
this.#operations = [...this.#operations, operation];
|
|
1694
|
+
this.#operations = [...this.#operations, { ...operation, status: "PENDING", attempts: 0 }];
|
|
1615
1695
|
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1696
|
+
this.#announceChange();
|
|
1616
1697
|
}
|
|
1617
1698
|
async enqueueCheckIn(request) {
|
|
1618
1699
|
return this.enqueue({ kind: "checkIn", request });
|
|
@@ -1624,6 +1705,7 @@ var OfflineQueue = class {
|
|
|
1624
1705
|
await this.initialize();
|
|
1625
1706
|
this.#operations = [];
|
|
1626
1707
|
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1708
|
+
this.#announceChange();
|
|
1627
1709
|
}
|
|
1628
1710
|
async sync(sender = this.#sender) {
|
|
1629
1711
|
await this.initialize();
|
|
@@ -1639,35 +1721,83 @@ var OfflineQueue = class {
|
|
|
1639
1721
|
return this.sync(sender);
|
|
1640
1722
|
}
|
|
1641
1723
|
async #run(sender) {
|
|
1724
|
+
const locks = globalThis.navigator?.locks;
|
|
1725
|
+
if (locks !== void 0) {
|
|
1726
|
+
let callbackStarted = false;
|
|
1727
|
+
try {
|
|
1728
|
+
const acquired = await locks.request(
|
|
1729
|
+
`stamprally:${this.#storageKey()}:sync`,
|
|
1730
|
+
{ ifAvailable: true },
|
|
1731
|
+
async (lock) => {
|
|
1732
|
+
if (lock === null) {
|
|
1733
|
+
await this.#reloadFromStorage();
|
|
1734
|
+
this.#state = "idle";
|
|
1735
|
+
return false;
|
|
1736
|
+
}
|
|
1737
|
+
callbackStarted = true;
|
|
1738
|
+
await this.#runWithStorageLock(sender);
|
|
1739
|
+
return true;
|
|
1740
|
+
}
|
|
1741
|
+
);
|
|
1742
|
+
if (!acquired) return;
|
|
1743
|
+
return;
|
|
1744
|
+
} catch (error) {
|
|
1745
|
+
if (callbackStarted) throw error;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
await this.#runWithStorageLock(sender);
|
|
1749
|
+
}
|
|
1750
|
+
async #runWithStorageLock(sender) {
|
|
1642
1751
|
this.#state = "syncing";
|
|
1643
1752
|
this.#error = null;
|
|
1753
|
+
if (!this.#acquireSyncLock()) {
|
|
1754
|
+
await this.#reloadFromStorage();
|
|
1755
|
+
this.#state = "idle";
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1644
1758
|
try {
|
|
1645
1759
|
while (this.#operations.length > 0) {
|
|
1646
1760
|
const operation = this.#operations[0];
|
|
1647
1761
|
if (operation === void 0) break;
|
|
1648
|
-
let
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1762
|
+
let attempt = 0;
|
|
1763
|
+
let response;
|
|
1764
|
+
while (true) {
|
|
1765
|
+
await this.#updateOperationStatus("IN_FLIGHT", attempt);
|
|
1766
|
+
try {
|
|
1767
|
+
response = this.#normalizeResponse(await sender(operation));
|
|
1768
|
+
} catch (cause) {
|
|
1769
|
+
response = {
|
|
1770
|
+
status: "RETRYABLE_ERROR",
|
|
1771
|
+
error: errorValue(cause, "RETRYABLE_ERROR")
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
if (response.status !== "RETRYABLE_ERROR") break;
|
|
1656
1775
|
const error2 = errorValue(response.error ?? response.reason, "RETRYABLE_ERROR");
|
|
1776
|
+
await this.#updateOperationStatus("PENDING", attempt + 1);
|
|
1657
1777
|
await this.#syncResultListener?.({ operation, status: response.status, error: error2 });
|
|
1658
|
-
throw new Error(error2.message);
|
|
1778
|
+
if (attempt >= this.#retryOptions.maxRetries) throw new Error(error2.message);
|
|
1779
|
+
const interval = this.#retryOptions.initialIntervalMs * this.#retryOptions.backoffMultiplier ** attempt;
|
|
1780
|
+
await new Promise((resolve) => setTimeout(resolve, Math.max(0, interval)));
|
|
1781
|
+
attempt += 1;
|
|
1659
1782
|
}
|
|
1660
1783
|
const result = response.result;
|
|
1661
1784
|
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);
|
|
1662
1785
|
const error = response.status === "REJECTED_PERMANENT" ? errorValue(response.error ?? response.reason, "REJECTED_PERMANENT") : void 0;
|
|
1786
|
+
await this.#updateOperationStatus(
|
|
1787
|
+
response.status === "ACCEPTED" ? "ACCEPTED" : "REJECTED",
|
|
1788
|
+
attempt + 1
|
|
1789
|
+
);
|
|
1790
|
+
const fallbackState = response.status === "REJECTED_PERMANENT" ? operation.request.state : void 0;
|
|
1791
|
+
const eventState = state ?? fallbackState;
|
|
1663
1792
|
this.#operations = this.#operations.slice(1);
|
|
1664
1793
|
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1794
|
+
this.#announceChange();
|
|
1665
1795
|
await this.#syncResultListener?.({
|
|
1666
1796
|
operation,
|
|
1667
1797
|
...result === void 0 ? {} : { result },
|
|
1668
1798
|
status: response.status,
|
|
1669
1799
|
...error === void 0 ? {} : { error },
|
|
1670
|
-
...
|
|
1800
|
+
...eventState === void 0 ? {} : { state: eventState }
|
|
1671
1801
|
});
|
|
1672
1802
|
}
|
|
1673
1803
|
this.#state = "idle";
|
|
@@ -1675,6 +1805,87 @@ var OfflineQueue = class {
|
|
|
1675
1805
|
this.#state = "error";
|
|
1676
1806
|
this.#error = cause instanceof Error ? cause : new Error(String(cause));
|
|
1677
1807
|
throw this.#error;
|
|
1808
|
+
} finally {
|
|
1809
|
+
this.#releaseSyncLock();
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
async #updateOperationStatus(status, attempts) {
|
|
1813
|
+
const operation = this.#operations[0];
|
|
1814
|
+
if (operation === void 0) return;
|
|
1815
|
+
this.#operations = [{ ...operation, status, attempts }, ...this.#operations.slice(1)];
|
|
1816
|
+
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1817
|
+
this.#announceChange();
|
|
1818
|
+
}
|
|
1819
|
+
#subscribeToExternalChanges() {
|
|
1820
|
+
const windowLike = globalThis.window;
|
|
1821
|
+
if (windowLike !== void 0) {
|
|
1822
|
+
this.#storageListener = (event) => {
|
|
1823
|
+
if (event.key === this.#storageKey()) void this.#reloadFromStorage();
|
|
1824
|
+
};
|
|
1825
|
+
windowLike.addEventListener("storage", this.#storageListener);
|
|
1826
|
+
}
|
|
1827
|
+
const Channel = globalThis.BroadcastChannel;
|
|
1828
|
+
if (Channel !== void 0) {
|
|
1829
|
+
try {
|
|
1830
|
+
this.#channel = new Channel("stamprally:queue-sync");
|
|
1831
|
+
this.#channel.addEventListener("message", (event) => {
|
|
1832
|
+
if (typeof event.data === "object" && event.data !== null && "key" in event.data && event.data.key === this.#storageKey())
|
|
1833
|
+
void this.#reloadFromStorage();
|
|
1834
|
+
});
|
|
1835
|
+
} catch {
|
|
1836
|
+
this.#channel = null;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
#announceChange() {
|
|
1841
|
+
this.#channel?.postMessage({ key: this.#storageKey(), owner: this.#instanceId });
|
|
1842
|
+
}
|
|
1843
|
+
async #reloadFromStorage() {
|
|
1844
|
+
if (this.#state === "syncing") return;
|
|
1845
|
+
try {
|
|
1846
|
+
this.#operations = (await this.#storage.load(this.#storageKey())).map(normalizeOperation);
|
|
1847
|
+
this.#loaded = true;
|
|
1848
|
+
} catch {
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
#lockKey() {
|
|
1852
|
+
return `${this.#storageKey()}:sync-lock`;
|
|
1853
|
+
}
|
|
1854
|
+
#acquireSyncLock() {
|
|
1855
|
+
const key = this.#lockKey();
|
|
1856
|
+
const now = Date.now();
|
|
1857
|
+
const local = syncLocks.get(key);
|
|
1858
|
+
if (local !== void 0 && local.expiresAt > now && local.owner !== this.#instanceId)
|
|
1859
|
+
return false;
|
|
1860
|
+
if (this.#lockStorage !== null) {
|
|
1861
|
+
try {
|
|
1862
|
+
const existing = this.#lockStorage.getItem(key);
|
|
1863
|
+
if (existing !== null) {
|
|
1864
|
+
const parsed = JSON.parse(existing);
|
|
1865
|
+
if (typeof parsed === "object" && parsed !== null && typeof parsed.expiresAt === "number" && parsed.expiresAt > now && parsed.owner !== this.#instanceId)
|
|
1866
|
+
return false;
|
|
1867
|
+
}
|
|
1868
|
+
this.#lockStorage.setItem(
|
|
1869
|
+
key,
|
|
1870
|
+
JSON.stringify({ owner: this.#instanceId, expiresAt: now + SYNC_LOCK_TTL_MS })
|
|
1871
|
+
);
|
|
1872
|
+
} catch {
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
syncLocks.set(key, { owner: this.#instanceId, expiresAt: now + SYNC_LOCK_TTL_MS });
|
|
1876
|
+
return true;
|
|
1877
|
+
}
|
|
1878
|
+
#releaseSyncLock() {
|
|
1879
|
+
const key = this.#lockKey();
|
|
1880
|
+
const current = syncLocks.get(key);
|
|
1881
|
+
if (current?.owner === this.#instanceId) syncLocks.delete(key);
|
|
1882
|
+
if (this.#lockStorage !== null) {
|
|
1883
|
+
try {
|
|
1884
|
+
const value = this.#lockStorage.getItem(key);
|
|
1885
|
+
if (value !== null && JSON.parse(value).owner === this.#instanceId)
|
|
1886
|
+
this.#lockStorage.removeItem?.(key);
|
|
1887
|
+
} catch {
|
|
1888
|
+
}
|
|
1678
1889
|
}
|
|
1679
1890
|
}
|
|
1680
1891
|
#storageKey() {
|
|
@@ -1702,6 +1913,13 @@ var OfflineQueue = class {
|
|
|
1702
1913
|
return resolveRallyStateConflict(serverState, localState, { policy });
|
|
1703
1914
|
}
|
|
1704
1915
|
};
|
|
1916
|
+
function normalizeOperation(operation) {
|
|
1917
|
+
return {
|
|
1918
|
+
...operation,
|
|
1919
|
+
status: operation.status === "IN_FLIGHT" ? "PENDING" : operation.status ?? "PENDING",
|
|
1920
|
+
attempts: operation.attempts ?? 0
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1705
1923
|
|
|
1706
1924
|
// src/crypto/token.ts
|
|
1707
1925
|
var encoder = new TextEncoder();
|
|
@@ -2270,6 +2488,8 @@ function condition(value, path, errors, isPublic) {
|
|
|
2270
2488
|
finiteNumber(value, "latitude", path, errors);
|
|
2271
2489
|
finiteNumber(value, "longitude", path, errors);
|
|
2272
2490
|
finiteNumber(value, "radiusMeters", path, errors, 0);
|
|
2491
|
+
if (typeof value.radiusMeters === "number" && value.radiusMeters <= 0)
|
|
2492
|
+
add(errors, `${path}.radiusMeters`, "Expected a radius greater than 0.", "out_of_range");
|
|
2273
2493
|
if (typeof value.latitude === "number" && (value.latitude < -90 || value.latitude > 90))
|
|
2274
2494
|
add(errors, `${path}.latitude`, "Expected a latitude between -90 and 90.", "out_of_range");
|
|
2275
2495
|
if (typeof value.longitude === "number" && (value.longitude < -180 || value.longitude > 180))
|
|
@@ -2437,21 +2657,112 @@ function validate(value, isPublic) {
|
|
|
2437
2657
|
optionalString(value, "staffPasscode", "$", errors);
|
|
2438
2658
|
if (hasOwn(value, "inventory") && value.inventory !== void 0 && !isRecord3(value.inventory))
|
|
2439
2659
|
add(errors, "$.inventory", "Expected an object.", "invalid_type");
|
|
2660
|
+
if (hasOwn(value, "inventoryMode") && value.inventoryMode !== void 0 && value.inventoryMode !== "shared" && value.inventoryMode !== "per_reward")
|
|
2661
|
+
add(errors, "$.inventoryMode", "Expected shared or per_reward.", "invalid_enum");
|
|
2440
2662
|
if (hasOwn(value, "serverMetadata") && value.serverMetadata !== void 0 && !isRecord3(value.serverMetadata))
|
|
2441
2663
|
add(errors, "$.serverMetadata", "Expected an object.", "invalid_type");
|
|
2442
2664
|
if (hasOwn(value, "publicMetadata") && value.publicMetadata !== void 0 && !isRecord3(value.publicMetadata))
|
|
2443
2665
|
add(errors, "$.publicMetadata", "Expected an object.", "invalid_type");
|
|
2444
2666
|
optionalString(value, "serverEndpoint", "$", errors);
|
|
2445
2667
|
} else {
|
|
2446
|
-
for (const key of ["staffPasscode", "serverMetadata", "inventory"])
|
|
2668
|
+
for (const key of ["staffPasscode", "serverMetadata", "inventory", "inventoryMode"])
|
|
2447
2669
|
if (hasOwn(value, key))
|
|
2448
2670
|
add(errors, `$.${key}`, "Private field is not allowed.", "private_field");
|
|
2449
2671
|
optionalString(value, "serverEndpoint", "$", errors);
|
|
2450
2672
|
}
|
|
2451
2673
|
return errors;
|
|
2452
2674
|
}
|
|
2675
|
+
function validateRallyConfigRelations(config) {
|
|
2676
|
+
const errors = [];
|
|
2677
|
+
const spotIds = /* @__PURE__ */ new Set();
|
|
2678
|
+
const rewardIds = /* @__PURE__ */ new Set();
|
|
2679
|
+
const orderIndexes = /* @__PURE__ */ new Map();
|
|
2680
|
+
config.spots.forEach((spot2, index) => {
|
|
2681
|
+
if (spotIds.has(spot2.id))
|
|
2682
|
+
add(errors, `spots[${index}].id`, "Spot ID must be unique.", "duplicate_spot_id");
|
|
2683
|
+
spotIds.add(spot2.id);
|
|
2684
|
+
const previousIndex = orderIndexes.get(spot2.orderIndex);
|
|
2685
|
+
if (previousIndex !== void 0)
|
|
2686
|
+
add(
|
|
2687
|
+
errors,
|
|
2688
|
+
`spots[${index}].orderIndex`,
|
|
2689
|
+
`orderIndex duplicates spots[${previousIndex}].`,
|
|
2690
|
+
"duplicate_order_index"
|
|
2691
|
+
);
|
|
2692
|
+
else orderIndexes.set(spot2.orderIndex, index);
|
|
2693
|
+
if (spot2.orderIndex < 0)
|
|
2694
|
+
add(
|
|
2695
|
+
errors,
|
|
2696
|
+
`spots[${index}].orderIndex`,
|
|
2697
|
+
"orderIndex must not be negative.",
|
|
2698
|
+
"negative_order_index"
|
|
2699
|
+
);
|
|
2700
|
+
spot2.prerequisites?.forEach((prerequisite, prerequisiteIndex) => {
|
|
2701
|
+
if (!spotIds.has(prerequisite) && !config.spots.some((candidate) => candidate.id === prerequisite))
|
|
2702
|
+
add(
|
|
2703
|
+
errors,
|
|
2704
|
+
`spots[${index}].prerequisites[${prerequisiteIndex}]`,
|
|
2705
|
+
"Prerequisite spot does not exist.",
|
|
2706
|
+
"missing_prerequisite"
|
|
2707
|
+
);
|
|
2708
|
+
});
|
|
2709
|
+
});
|
|
2710
|
+
config.rewards.forEach((reward2, index) => {
|
|
2711
|
+
if (rewardIds.has(reward2.id))
|
|
2712
|
+
add(errors, `rewards[${index}].id`, "Reward ID must be unique.", "duplicate_reward_id");
|
|
2713
|
+
rewardIds.add(reward2.id);
|
|
2714
|
+
const visit = (condition2, path) => {
|
|
2715
|
+
if (condition2.type === "stamps")
|
|
2716
|
+
condition2.stampIds.forEach((stampId, stampIndex) => {
|
|
2717
|
+
if (!spotIds.has(stampId))
|
|
2718
|
+
add(
|
|
2719
|
+
errors,
|
|
2720
|
+
`${path}.stampIds[${stampIndex}]`,
|
|
2721
|
+
"Referenced spot does not exist.",
|
|
2722
|
+
"missing_reward_spot"
|
|
2723
|
+
);
|
|
2724
|
+
});
|
|
2725
|
+
else if (condition2.type === "all" || condition2.type === "any")
|
|
2726
|
+
condition2.conditions.forEach((nested, nestedIndex) => {
|
|
2727
|
+
visit(nested, `${path}.conditions[${nestedIndex}]`);
|
|
2728
|
+
});
|
|
2729
|
+
};
|
|
2730
|
+
reward2.conditions?.forEach((condition2, conditionIndex) => {
|
|
2731
|
+
visit(condition2, `rewards[${index}].conditions[${conditionIndex}]`);
|
|
2732
|
+
});
|
|
2733
|
+
});
|
|
2734
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
2735
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2736
|
+
const cycleNodes = /* @__PURE__ */ new Set();
|
|
2737
|
+
const visitSpot = (spotId) => {
|
|
2738
|
+
if (visiting.has(spotId)) {
|
|
2739
|
+
cycleNodes.add(spotId);
|
|
2740
|
+
return;
|
|
2741
|
+
}
|
|
2742
|
+
if (visited.has(spotId)) return;
|
|
2743
|
+
visiting.add(spotId);
|
|
2744
|
+
const spot2 = config.spots.find((candidate) => candidate.id === spotId);
|
|
2745
|
+
spot2?.prerequisites?.forEach(visitSpot);
|
|
2746
|
+
visiting.delete(spotId);
|
|
2747
|
+
visited.add(spotId);
|
|
2748
|
+
};
|
|
2749
|
+
config.spots.forEach((spot2) => {
|
|
2750
|
+
visitSpot(spot2.id);
|
|
2751
|
+
});
|
|
2752
|
+
cycleNodes.forEach((spotId) => {
|
|
2753
|
+
const index = config.spots.findIndex((spot2) => spot2.id === spotId);
|
|
2754
|
+
add(
|
|
2755
|
+
errors,
|
|
2756
|
+
`spots[${index}].prerequisites`,
|
|
2757
|
+
"Prerequisites must form a DAG.",
|
|
2758
|
+
"cyclic_prerequisites"
|
|
2759
|
+
);
|
|
2760
|
+
});
|
|
2761
|
+
return errors;
|
|
2762
|
+
}
|
|
2453
2763
|
function safeParseAdminConfig(input) {
|
|
2454
|
-
const errors = validate(input, false);
|
|
2764
|
+
const errors = [...validate(input, false)];
|
|
2765
|
+
if (errors.length === 0) errors.push(...validateRallyConfigRelations(input));
|
|
2455
2766
|
return errors.length === 0 ? { success: true, data: input } : { success: false, errors };
|
|
2456
2767
|
}
|
|
2457
2768
|
function parseAdminConfig(input) {
|
|
@@ -2460,7 +2771,8 @@ function parseAdminConfig(input) {
|
|
|
2460
2771
|
return result.data;
|
|
2461
2772
|
}
|
|
2462
2773
|
function safeParsePublicConfig(input) {
|
|
2463
|
-
const errors = validate(input, true);
|
|
2774
|
+
const errors = [...validate(input, true)];
|
|
2775
|
+
if (errors.length === 0) errors.push(...validateRallyConfigRelations(input));
|
|
2464
2776
|
return errors.length === 0 ? { success: true, data: input } : { success: false, errors };
|
|
2465
2777
|
}
|
|
2466
2778
|
function parsePublicConfig(input) {
|
|
@@ -2588,6 +2900,6 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
|
|
|
2588
2900
|
}
|
|
2589
2901
|
}
|
|
2590
2902
|
|
|
2591
|
-
export { ConfigValidationError, DEFAULT_SHEET_THEME, MemoryQueueStorage as InMemoryOfflineQueueStorage, InMemoryStorage, IndexedDBAdapter, IndexedDBOfflineQueueStorage, LocalStorageAdapter, OfflineQueue, StampRallyClient, StorageAdapterError, THEME_PRESETS, assertPublicConfig, calculateDistanceMeters, calculateProgress, consumeReward, 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, verifyPasscode, verifySecureToken, verifySnapshotToken };
|
|
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 };
|
|
2592
2904
|
//# sourceMappingURL=index.js.map
|
|
2593
2905
|
//# sourceMappingURL=index.js.map
|