@stamprally/core 0.12.0 → 0.14.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 +13 -0
- package/dist/index.cjs +356 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -4
- package/dist/index.d.ts +51 -4
- package/dist/index.js +354 -30
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -39,6 +39,14 @@ function evaluateConditionDetailed(condition2, context) {
|
|
|
39
39
|
function evaluateCondition(condition2, context) {
|
|
40
40
|
return evaluateConditionDetailed(condition2, context).ok;
|
|
41
41
|
}
|
|
42
|
+
function evaluateSpotStatus(spot2, state, options = {}) {
|
|
43
|
+
if (options.verifying === true) return "VERIFYING";
|
|
44
|
+
if (state.records.some((record) => record.stampId === spot2.id)) return "CLAIMED";
|
|
45
|
+
const acquired = new Set(state.records.map((record) => record.stampId));
|
|
46
|
+
if (spot2.prerequisites?.some((prerequisite) => !acquired.has(prerequisite))) return "LOCKED";
|
|
47
|
+
return "UNCLAIMED";
|
|
48
|
+
}
|
|
49
|
+
var getSpotStatus = evaluateSpotStatus;
|
|
42
50
|
|
|
43
51
|
// src/engine/order.ts
|
|
44
52
|
function getOrderedSpots(spots) {
|
|
@@ -51,7 +59,9 @@ function calculateProgress(state, config) {
|
|
|
51
59
|
const acquired = new Set(
|
|
52
60
|
state.records.map((record) => record.stampId).filter((id2) => ids.has(id2))
|
|
53
61
|
);
|
|
54
|
-
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
|
+
);
|
|
55
65
|
return {
|
|
56
66
|
acquired: acquired.size,
|
|
57
67
|
total: config.spots.length,
|
|
@@ -115,24 +125,53 @@ function mergeRewardStates(serverRewards, localRewards) {
|
|
|
115
125
|
merged.set(localReward.rewardId, localReward);
|
|
116
126
|
continue;
|
|
117
127
|
}
|
|
118
|
-
|
|
119
|
-
|
|
128
|
+
const winner = localReward.status === "CONSUMED" && serverReward.status !== "CONSUMED" ? localReward : serverReward;
|
|
129
|
+
merged.set(localReward.rewardId, {
|
|
130
|
+
...winner,
|
|
131
|
+
...serverReward.unlockedAt === void 0 && localReward.unlockedAt === void 0 ? {} : {
|
|
132
|
+
unlockedAt: serverReward.unlockedAt === void 0 ? localReward.unlockedAt : localReward.unlockedAt === void 0 ? serverReward.unlockedAt : latestTimestamp(serverReward.unlockedAt, localReward.unlockedAt)
|
|
133
|
+
},
|
|
134
|
+
...serverReward.consumedAt === void 0 && localReward.consumedAt === void 0 ? {} : {
|
|
135
|
+
consumedAt: serverReward.consumedAt === void 0 ? localReward.consumedAt : localReward.consumedAt === void 0 ? serverReward.consumedAt : latestTimestamp(serverReward.consumedAt, localReward.consumedAt)
|
|
136
|
+
},
|
|
137
|
+
...serverReward.redeemedCount === void 0 && localReward.redeemedCount === void 0 ? {} : {
|
|
138
|
+
redeemedCount: Math.max(
|
|
139
|
+
serverReward.redeemedCount ?? 0,
|
|
140
|
+
localReward.redeemedCount ?? 0
|
|
141
|
+
)
|
|
142
|
+
},
|
|
143
|
+
...serverReward.userRedemptionCount === void 0 && localReward.userRedemptionCount === void 0 ? {} : {
|
|
144
|
+
userRedemptionCount: Math.max(
|
|
145
|
+
serverReward.userRedemptionCount ?? 0,
|
|
146
|
+
localReward.userRedemptionCount ?? 0
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
});
|
|
120
150
|
}
|
|
121
151
|
return [...merged.values()];
|
|
122
152
|
}
|
|
123
|
-
function
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
knownStamps.add(record.stampId);
|
|
153
|
+
function mergeStampRecords(serverRecords, localRecords) {
|
|
154
|
+
const merged = /* @__PURE__ */ new Map();
|
|
155
|
+
for (const record of [...serverRecords, ...localRecords]) {
|
|
156
|
+
const current = merged.get(record.stampId);
|
|
157
|
+
if (current === void 0) {
|
|
158
|
+
merged.set(record.stampId, record);
|
|
159
|
+
continue;
|
|
131
160
|
}
|
|
161
|
+
const acquiredAt = latestTimestamp(current.acquiredAt, record.acquiredAt);
|
|
162
|
+
merged.set(record.stampId, {
|
|
163
|
+
...current,
|
|
164
|
+
...acquiredAt === current.acquiredAt ? {} : { acquiredAt },
|
|
165
|
+
...current.metadata === void 0 && record.metadata !== void 0 ? { metadata: record.metadata } : {}
|
|
166
|
+
});
|
|
132
167
|
}
|
|
168
|
+
return [...merged.values()];
|
|
169
|
+
}
|
|
170
|
+
function resolveRallyStateConflict(serverState, localState, options = { policy: "merge" }) {
|
|
171
|
+
if (options.policy === "server_wins") return serverState;
|
|
133
172
|
return {
|
|
134
173
|
...serverState,
|
|
135
|
-
records,
|
|
174
|
+
records: mergeStampRecords(serverState.records, localState.records),
|
|
136
175
|
rewards: mergeRewardStates(serverState.rewards, localState.rewards),
|
|
137
176
|
updatedAt: latestTimestamp(serverState.updatedAt, localState.updatedAt)
|
|
138
177
|
};
|
|
@@ -1070,7 +1109,10 @@ var StampRallyClient = class {
|
|
|
1070
1109
|
initialize() {
|
|
1071
1110
|
if (this.#state !== null) return Promise.resolve(this.#state);
|
|
1072
1111
|
if (this.#initialization === null) {
|
|
1073
|
-
this.#initialization =
|
|
1112
|
+
this.#initialization = (async () => {
|
|
1113
|
+
await this.#offlineQueue?.setScope(this.#config.id, this.#userId);
|
|
1114
|
+
return this.#storage.load(this.#config.id, this.#userId);
|
|
1115
|
+
})().then((state) => {
|
|
1074
1116
|
const next = state === null ? emptyState(this.#config, this.#userId, this.#now()) : this.#reconcile(state);
|
|
1075
1117
|
this.#state = next;
|
|
1076
1118
|
this.#emit(next);
|
|
@@ -1088,6 +1130,7 @@ var StampRallyClient = class {
|
|
|
1088
1130
|
this.#userId = newUserId;
|
|
1089
1131
|
this.#state = null;
|
|
1090
1132
|
this.#initialization = null;
|
|
1133
|
+
await this.#offlineQueue?.switchUser(newUserId);
|
|
1091
1134
|
return this.initialize();
|
|
1092
1135
|
});
|
|
1093
1136
|
}
|
|
@@ -1336,7 +1379,8 @@ var StampRallyClient = class {
|
|
|
1336
1379
|
this.#state = next;
|
|
1337
1380
|
this.#emit(next);
|
|
1338
1381
|
}
|
|
1339
|
-
if ("
|
|
1382
|
+
if (event.error !== void 0) this.#emitEvent({ type: "error", error: event.error });
|
|
1383
|
+
else if (event.result !== void 0 && "ok" in event.result && !event.result.ok)
|
|
1340
1384
|
this.#emitEvent({ type: "error", error: event.result.error });
|
|
1341
1385
|
}
|
|
1342
1386
|
#now() {
|
|
@@ -1464,9 +1508,32 @@ function defaultStorage(databaseName) {
|
|
|
1464
1508
|
function operationId(operation) {
|
|
1465
1509
|
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}`;
|
|
1466
1510
|
}
|
|
1511
|
+
function requestScope(operation) {
|
|
1512
|
+
return {
|
|
1513
|
+
rallyId: operation.request.rallyId,
|
|
1514
|
+
userId: operation.request.userId
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
function errorValue(value, fallbackCode) {
|
|
1518
|
+
if (typeof value === "object" && value !== null) {
|
|
1519
|
+
const candidate = value;
|
|
1520
|
+
if (typeof candidate.code === "string" && typeof candidate.message === "string")
|
|
1521
|
+
return { ...candidate, code: candidate.code, message: candidate.message };
|
|
1522
|
+
}
|
|
1523
|
+
if (value instanceof Error) return { code: fallbackCode, message: value.message };
|
|
1524
|
+
if (typeof value === "string") return { code: fallbackCode, message: value };
|
|
1525
|
+
return { code: fallbackCode, message: "Offline operation was rejected." };
|
|
1526
|
+
}
|
|
1527
|
+
var syncLocks = /* @__PURE__ */ new Map();
|
|
1528
|
+
var SYNC_LOCK_TTL_MS = 3e4;
|
|
1529
|
+
function randomId() {
|
|
1530
|
+
return globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1531
|
+
}
|
|
1467
1532
|
var OfflineQueue = class {
|
|
1468
1533
|
#storage;
|
|
1469
|
-
#
|
|
1534
|
+
#configuredKey;
|
|
1535
|
+
#rallyId;
|
|
1536
|
+
#userId;
|
|
1470
1537
|
#conflictPolicy;
|
|
1471
1538
|
#onSyncConflict;
|
|
1472
1539
|
#operations = [];
|
|
@@ -1476,14 +1543,24 @@ var OfflineQueue = class {
|
|
|
1476
1543
|
#sender;
|
|
1477
1544
|
#syncPromise = null;
|
|
1478
1545
|
#syncResultListener;
|
|
1546
|
+
#synchronizeInstances;
|
|
1547
|
+
#instanceId = randomId();
|
|
1548
|
+
#lockStorage;
|
|
1549
|
+
#storageListener;
|
|
1550
|
+
#channel = null;
|
|
1479
1551
|
constructor(options = {}) {
|
|
1480
1552
|
if (options.storage !== void 0) this.#storage = options.storage;
|
|
1481
1553
|
else if (options.storageLike !== void 0 && options.storageLike !== null)
|
|
1482
1554
|
this.#storage = new LocalStorageQueueStorage(options.storageLike);
|
|
1483
1555
|
else this.#storage = defaultStorage(options.databaseName);
|
|
1484
|
-
this.#
|
|
1556
|
+
this.#configuredKey = options.key;
|
|
1557
|
+
this.#rallyId = options.rallyId;
|
|
1558
|
+
this.#userId = options.userId ?? null;
|
|
1485
1559
|
this.#conflictPolicy = options.conflictPolicy ?? "server_wins";
|
|
1486
1560
|
this.#onSyncConflict = options.onSyncConflict;
|
|
1561
|
+
this.#synchronizeInstances = options.synchronizeInstances ?? true;
|
|
1562
|
+
this.#lockStorage = options.storageLike ?? globalThis.localStorage ?? null;
|
|
1563
|
+
if (this.#synchronizeInstances) this.#subscribeToExternalChanges();
|
|
1487
1564
|
}
|
|
1488
1565
|
get syncState() {
|
|
1489
1566
|
return this.#state;
|
|
@@ -1500,23 +1577,68 @@ var OfflineQueue = class {
|
|
|
1500
1577
|
get conflictPolicy() {
|
|
1501
1578
|
return this.#conflictPolicy;
|
|
1502
1579
|
}
|
|
1580
|
+
get storageKey() {
|
|
1581
|
+
return this.#storageKey();
|
|
1582
|
+
}
|
|
1583
|
+
get rallyId() {
|
|
1584
|
+
return this.#rallyId;
|
|
1585
|
+
}
|
|
1586
|
+
get userId() {
|
|
1587
|
+
return this.#userId;
|
|
1588
|
+
}
|
|
1503
1589
|
setSyncResultListener(listener) {
|
|
1504
1590
|
this.#syncResultListener = listener;
|
|
1505
1591
|
}
|
|
1506
1592
|
async initialize() {
|
|
1507
1593
|
if (this.#loaded) return;
|
|
1508
|
-
this.#operations = [...await this.#storage.load(this.#
|
|
1594
|
+
this.#operations = [...await this.#storage.load(this.#storageKey())];
|
|
1509
1595
|
this.#loaded = true;
|
|
1510
1596
|
}
|
|
1597
|
+
/** Releases browser listeners when the queue is no longer used. */
|
|
1598
|
+
dispose() {
|
|
1599
|
+
const windowLike = globalThis.window;
|
|
1600
|
+
if (windowLike !== void 0 && this.#storageListener !== void 0)
|
|
1601
|
+
windowLike.removeEventListener("storage", this.#storageListener);
|
|
1602
|
+
this.#storageListener = void 0;
|
|
1603
|
+
this.#channel?.close();
|
|
1604
|
+
this.#channel = null;
|
|
1605
|
+
this.#releaseSyncLock();
|
|
1606
|
+
}
|
|
1607
|
+
/** Selects a rally/user queue scope and loads its pending operations. */
|
|
1608
|
+
async setScope(rallyId, userId) {
|
|
1609
|
+
if (this.#configuredKey !== void 0) {
|
|
1610
|
+
this.#rallyId = rallyId;
|
|
1611
|
+
this.#userId = userId;
|
|
1612
|
+
return this.initialize();
|
|
1613
|
+
}
|
|
1614
|
+
if (this.#rallyId === rallyId && this.#userId === userId && this.#loaded) return;
|
|
1615
|
+
this.#rallyId = rallyId;
|
|
1616
|
+
this.#userId = userId;
|
|
1617
|
+
this.#operations = [];
|
|
1618
|
+
this.#loaded = false;
|
|
1619
|
+
await this.initialize();
|
|
1620
|
+
}
|
|
1621
|
+
async switchUser(newUserId) {
|
|
1622
|
+
if (this.#rallyId === void 0)
|
|
1623
|
+
throw new Error("OfflineQueue.switchUser requires a rally scope.");
|
|
1624
|
+
await this.setScope(this.#rallyId, newUserId);
|
|
1625
|
+
}
|
|
1511
1626
|
setSender(sender) {
|
|
1512
1627
|
this.#sender = sender;
|
|
1513
1628
|
}
|
|
1514
1629
|
async enqueue(operation) {
|
|
1630
|
+
if (this.#configuredKey === void 0) {
|
|
1631
|
+
const scope = requestScope(operation);
|
|
1632
|
+
if (this.#rallyId === void 0) await this.setScope(scope.rallyId, scope.userId);
|
|
1633
|
+
if (this.#rallyId !== scope.rallyId || this.#userId !== scope.userId)
|
|
1634
|
+
throw new Error("Offline operation belongs to another rally or user queue.");
|
|
1635
|
+
}
|
|
1515
1636
|
await this.initialize();
|
|
1516
1637
|
const id2 = operationId(operation);
|
|
1517
1638
|
if (this.#operations.some((item) => operationId(item) === id2)) return;
|
|
1518
1639
|
this.#operations = [...this.#operations, operation];
|
|
1519
|
-
await this.#storage.save(this.#
|
|
1640
|
+
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1641
|
+
this.#announceChange();
|
|
1520
1642
|
}
|
|
1521
1643
|
async enqueueCheckIn(request) {
|
|
1522
1644
|
return this.enqueue({ kind: "checkIn", request });
|
|
@@ -1527,7 +1649,8 @@ var OfflineQueue = class {
|
|
|
1527
1649
|
async clear() {
|
|
1528
1650
|
await this.initialize();
|
|
1529
1651
|
this.#operations = [];
|
|
1530
|
-
await this.#storage.save(this.#
|
|
1652
|
+
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1653
|
+
this.#announceChange();
|
|
1531
1654
|
}
|
|
1532
1655
|
async sync(sender = this.#sender) {
|
|
1533
1656
|
await this.initialize();
|
|
@@ -1545,31 +1668,140 @@ var OfflineQueue = class {
|
|
|
1545
1668
|
async #run(sender) {
|
|
1546
1669
|
this.#state = "syncing";
|
|
1547
1670
|
this.#error = null;
|
|
1671
|
+
if (!this.#acquireSyncLock()) {
|
|
1672
|
+
await this.#reloadFromStorage();
|
|
1673
|
+
this.#state = "idle";
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1548
1676
|
try {
|
|
1549
1677
|
while (this.#operations.length > 0) {
|
|
1550
1678
|
const operation = this.#operations[0];
|
|
1551
1679
|
if (operation === void 0) break;
|
|
1552
|
-
let
|
|
1680
|
+
let rawResult;
|
|
1553
1681
|
try {
|
|
1554
|
-
|
|
1682
|
+
rawResult = await sender(operation);
|
|
1555
1683
|
} catch (cause) {
|
|
1556
|
-
throw
|
|
1684
|
+
throw new Error(errorValue(cause, "RETRYABLE_ERROR").message);
|
|
1557
1685
|
}
|
|
1558
|
-
const
|
|
1686
|
+
const response = this.#normalizeResponse(rawResult);
|
|
1687
|
+
if (response.status === "RETRYABLE_ERROR") {
|
|
1688
|
+
const error2 = errorValue(response.error ?? response.reason, "RETRYABLE_ERROR");
|
|
1689
|
+
await this.#syncResultListener?.({ operation, status: response.status, error: error2 });
|
|
1690
|
+
throw new Error(error2.message);
|
|
1691
|
+
}
|
|
1692
|
+
const result = response.result;
|
|
1693
|
+
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);
|
|
1694
|
+
const error = response.status === "REJECTED_PERMANENT" ? errorValue(response.error ?? response.reason, "REJECTED_PERMANENT") : void 0;
|
|
1695
|
+
this.#operations = this.#operations.slice(1);
|
|
1696
|
+
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1697
|
+
this.#announceChange();
|
|
1559
1698
|
await this.#syncResultListener?.({
|
|
1560
1699
|
operation,
|
|
1561
|
-
result,
|
|
1700
|
+
...result === void 0 ? {} : { result },
|
|
1701
|
+
status: response.status,
|
|
1702
|
+
...error === void 0 ? {} : { error },
|
|
1562
1703
|
...state === void 0 ? {} : { state }
|
|
1563
1704
|
});
|
|
1564
|
-
this.#operations = this.#operations.slice(1);
|
|
1565
|
-
await this.#storage.save(this.#key, this.#operations);
|
|
1566
1705
|
}
|
|
1567
1706
|
this.#state = "idle";
|
|
1568
1707
|
} catch (cause) {
|
|
1569
1708
|
this.#state = "error";
|
|
1570
1709
|
this.#error = cause instanceof Error ? cause : new Error(String(cause));
|
|
1571
1710
|
throw this.#error;
|
|
1711
|
+
} finally {
|
|
1712
|
+
this.#releaseSyncLock();
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
#subscribeToExternalChanges() {
|
|
1716
|
+
const windowLike = globalThis.window;
|
|
1717
|
+
if (windowLike !== void 0) {
|
|
1718
|
+
this.#storageListener = (event) => {
|
|
1719
|
+
if (event.key === this.#storageKey()) void this.#reloadFromStorage();
|
|
1720
|
+
};
|
|
1721
|
+
windowLike.addEventListener("storage", this.#storageListener);
|
|
1572
1722
|
}
|
|
1723
|
+
const Channel = globalThis.BroadcastChannel;
|
|
1724
|
+
if (Channel !== void 0) {
|
|
1725
|
+
try {
|
|
1726
|
+
this.#channel = new Channel("stamprally:queue-sync");
|
|
1727
|
+
this.#channel.addEventListener("message", (event) => {
|
|
1728
|
+
if (typeof event.data === "object" && event.data !== null && "key" in event.data && event.data.key === this.#storageKey())
|
|
1729
|
+
void this.#reloadFromStorage();
|
|
1730
|
+
});
|
|
1731
|
+
} catch {
|
|
1732
|
+
this.#channel = null;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
#announceChange() {
|
|
1737
|
+
this.#channel?.postMessage({ key: this.#storageKey(), owner: this.#instanceId });
|
|
1738
|
+
}
|
|
1739
|
+
async #reloadFromStorage() {
|
|
1740
|
+
if (this.#state === "syncing") return;
|
|
1741
|
+
try {
|
|
1742
|
+
this.#operations = [...await this.#storage.load(this.#storageKey())];
|
|
1743
|
+
this.#loaded = true;
|
|
1744
|
+
} catch {
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
#lockKey() {
|
|
1748
|
+
return `${this.#storageKey()}:sync-lock`;
|
|
1749
|
+
}
|
|
1750
|
+
#acquireSyncLock() {
|
|
1751
|
+
const key = this.#lockKey();
|
|
1752
|
+
const now = Date.now();
|
|
1753
|
+
const local = syncLocks.get(key);
|
|
1754
|
+
if (local !== void 0 && local.expiresAt > now && local.owner !== this.#instanceId)
|
|
1755
|
+
return false;
|
|
1756
|
+
if (this.#lockStorage !== null) {
|
|
1757
|
+
try {
|
|
1758
|
+
const existing = this.#lockStorage.getItem(key);
|
|
1759
|
+
if (existing !== null) {
|
|
1760
|
+
const parsed = JSON.parse(existing);
|
|
1761
|
+
if (typeof parsed === "object" && parsed !== null && typeof parsed.expiresAt === "number" && parsed.expiresAt > now && parsed.owner !== this.#instanceId)
|
|
1762
|
+
return false;
|
|
1763
|
+
}
|
|
1764
|
+
this.#lockStorage.setItem(
|
|
1765
|
+
key,
|
|
1766
|
+
JSON.stringify({ owner: this.#instanceId, expiresAt: now + SYNC_LOCK_TTL_MS })
|
|
1767
|
+
);
|
|
1768
|
+
} catch {
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
syncLocks.set(key, { owner: this.#instanceId, expiresAt: now + SYNC_LOCK_TTL_MS });
|
|
1772
|
+
return true;
|
|
1773
|
+
}
|
|
1774
|
+
#releaseSyncLock() {
|
|
1775
|
+
const key = this.#lockKey();
|
|
1776
|
+
const current = syncLocks.get(key);
|
|
1777
|
+
if (current?.owner === this.#instanceId) syncLocks.delete(key);
|
|
1778
|
+
if (this.#lockStorage !== null) {
|
|
1779
|
+
try {
|
|
1780
|
+
const value = this.#lockStorage.getItem(key);
|
|
1781
|
+
if (value !== null && JSON.parse(value).owner === this.#instanceId)
|
|
1782
|
+
this.#lockStorage.removeItem?.(key);
|
|
1783
|
+
} catch {
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
#storageKey() {
|
|
1788
|
+
if (this.#configuredKey !== void 0) return this.#configuredKey;
|
|
1789
|
+
return `stamprally:queue:${this.#rallyId ?? "unscoped"}:${this.#userId ?? "anonymous"}`;
|
|
1790
|
+
}
|
|
1791
|
+
#normalizeResponse(value) {
|
|
1792
|
+
if ("ok" in value) {
|
|
1793
|
+
if (value.ok === false) {
|
|
1794
|
+
if ("status" in value && value.status === "RETRYABLE_ERROR")
|
|
1795
|
+
return { status: "RETRYABLE_ERROR", error: errorValue(value.error, "RETRYABLE_ERROR") };
|
|
1796
|
+
return { status: "REJECTED_PERMANENT", result: value };
|
|
1797
|
+
}
|
|
1798
|
+
return { status: "ACCEPTED", result: value };
|
|
1799
|
+
}
|
|
1800
|
+
if ("status" in value) {
|
|
1801
|
+
if (value.status === "ACCEPTED") return value;
|
|
1802
|
+
return value;
|
|
1803
|
+
}
|
|
1804
|
+
return { status: "ACCEPTED", result: value };
|
|
1573
1805
|
}
|
|
1574
1806
|
async resolveConflict(operation, localState, serverState) {
|
|
1575
1807
|
const configured = this.#onSyncConflict;
|
|
@@ -2312,21 +2544,112 @@ function validate(value, isPublic) {
|
|
|
2312
2544
|
optionalString(value, "staffPasscode", "$", errors);
|
|
2313
2545
|
if (hasOwn(value, "inventory") && value.inventory !== void 0 && !isRecord3(value.inventory))
|
|
2314
2546
|
add(errors, "$.inventory", "Expected an object.", "invalid_type");
|
|
2547
|
+
if (hasOwn(value, "inventoryMode") && value.inventoryMode !== void 0 && value.inventoryMode !== "shared" && value.inventoryMode !== "per_reward")
|
|
2548
|
+
add(errors, "$.inventoryMode", "Expected shared or per_reward.", "invalid_enum");
|
|
2315
2549
|
if (hasOwn(value, "serverMetadata") && value.serverMetadata !== void 0 && !isRecord3(value.serverMetadata))
|
|
2316
2550
|
add(errors, "$.serverMetadata", "Expected an object.", "invalid_type");
|
|
2317
2551
|
if (hasOwn(value, "publicMetadata") && value.publicMetadata !== void 0 && !isRecord3(value.publicMetadata))
|
|
2318
2552
|
add(errors, "$.publicMetadata", "Expected an object.", "invalid_type");
|
|
2319
2553
|
optionalString(value, "serverEndpoint", "$", errors);
|
|
2320
2554
|
} else {
|
|
2321
|
-
for (const key of ["staffPasscode", "serverMetadata", "inventory"])
|
|
2555
|
+
for (const key of ["staffPasscode", "serverMetadata", "inventory", "inventoryMode"])
|
|
2322
2556
|
if (hasOwn(value, key))
|
|
2323
2557
|
add(errors, `$.${key}`, "Private field is not allowed.", "private_field");
|
|
2324
2558
|
optionalString(value, "serverEndpoint", "$", errors);
|
|
2325
2559
|
}
|
|
2326
2560
|
return errors;
|
|
2327
2561
|
}
|
|
2562
|
+
function validateRallyConfigRelations(config) {
|
|
2563
|
+
const errors = [];
|
|
2564
|
+
const spotIds = /* @__PURE__ */ new Set();
|
|
2565
|
+
const rewardIds = /* @__PURE__ */ new Set();
|
|
2566
|
+
const orderIndexes = /* @__PURE__ */ new Map();
|
|
2567
|
+
config.spots.forEach((spot2, index) => {
|
|
2568
|
+
if (spotIds.has(spot2.id))
|
|
2569
|
+
add(errors, `spots[${index}].id`, "Spot ID must be unique.", "duplicate_spot_id");
|
|
2570
|
+
spotIds.add(spot2.id);
|
|
2571
|
+
const previousIndex = orderIndexes.get(spot2.orderIndex);
|
|
2572
|
+
if (previousIndex !== void 0)
|
|
2573
|
+
add(
|
|
2574
|
+
errors,
|
|
2575
|
+
`spots[${index}].orderIndex`,
|
|
2576
|
+
`orderIndex duplicates spots[${previousIndex}].`,
|
|
2577
|
+
"duplicate_order_index"
|
|
2578
|
+
);
|
|
2579
|
+
else orderIndexes.set(spot2.orderIndex, index);
|
|
2580
|
+
if (spot2.orderIndex < 0)
|
|
2581
|
+
add(
|
|
2582
|
+
errors,
|
|
2583
|
+
`spots[${index}].orderIndex`,
|
|
2584
|
+
"orderIndex must not be negative.",
|
|
2585
|
+
"negative_order_index"
|
|
2586
|
+
);
|
|
2587
|
+
spot2.prerequisites?.forEach((prerequisite, prerequisiteIndex) => {
|
|
2588
|
+
if (!spotIds.has(prerequisite) && !config.spots.some((candidate) => candidate.id === prerequisite))
|
|
2589
|
+
add(
|
|
2590
|
+
errors,
|
|
2591
|
+
`spots[${index}].prerequisites[${prerequisiteIndex}]`,
|
|
2592
|
+
"Prerequisite spot does not exist.",
|
|
2593
|
+
"missing_prerequisite"
|
|
2594
|
+
);
|
|
2595
|
+
});
|
|
2596
|
+
});
|
|
2597
|
+
config.rewards.forEach((reward2, index) => {
|
|
2598
|
+
if (rewardIds.has(reward2.id))
|
|
2599
|
+
add(errors, `rewards[${index}].id`, "Reward ID must be unique.", "duplicate_reward_id");
|
|
2600
|
+
rewardIds.add(reward2.id);
|
|
2601
|
+
const visit = (condition2, path) => {
|
|
2602
|
+
if (condition2.type === "stamps")
|
|
2603
|
+
condition2.stampIds.forEach((stampId, stampIndex) => {
|
|
2604
|
+
if (!spotIds.has(stampId))
|
|
2605
|
+
add(
|
|
2606
|
+
errors,
|
|
2607
|
+
`${path}.stampIds[${stampIndex}]`,
|
|
2608
|
+
"Referenced spot does not exist.",
|
|
2609
|
+
"missing_reward_spot"
|
|
2610
|
+
);
|
|
2611
|
+
});
|
|
2612
|
+
else if (condition2.type === "all" || condition2.type === "any")
|
|
2613
|
+
condition2.conditions.forEach((nested, nestedIndex) => {
|
|
2614
|
+
visit(nested, `${path}.conditions[${nestedIndex}]`);
|
|
2615
|
+
});
|
|
2616
|
+
};
|
|
2617
|
+
reward2.conditions?.forEach((condition2, conditionIndex) => {
|
|
2618
|
+
visit(condition2, `rewards[${index}].conditions[${conditionIndex}]`);
|
|
2619
|
+
});
|
|
2620
|
+
});
|
|
2621
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
2622
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2623
|
+
const cycleNodes = /* @__PURE__ */ new Set();
|
|
2624
|
+
const visitSpot = (spotId) => {
|
|
2625
|
+
if (visiting.has(spotId)) {
|
|
2626
|
+
cycleNodes.add(spotId);
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
if (visited.has(spotId)) return;
|
|
2630
|
+
visiting.add(spotId);
|
|
2631
|
+
const spot2 = config.spots.find((candidate) => candidate.id === spotId);
|
|
2632
|
+
spot2?.prerequisites?.forEach(visitSpot);
|
|
2633
|
+
visiting.delete(spotId);
|
|
2634
|
+
visited.add(spotId);
|
|
2635
|
+
};
|
|
2636
|
+
config.spots.forEach((spot2) => {
|
|
2637
|
+
visitSpot(spot2.id);
|
|
2638
|
+
});
|
|
2639
|
+
cycleNodes.forEach((spotId) => {
|
|
2640
|
+
const index = config.spots.findIndex((spot2) => spot2.id === spotId);
|
|
2641
|
+
add(
|
|
2642
|
+
errors,
|
|
2643
|
+
`spots[${index}].prerequisites`,
|
|
2644
|
+
"Prerequisites must form a DAG.",
|
|
2645
|
+
"cyclic_prerequisites"
|
|
2646
|
+
);
|
|
2647
|
+
});
|
|
2648
|
+
return errors;
|
|
2649
|
+
}
|
|
2328
2650
|
function safeParseAdminConfig(input) {
|
|
2329
|
-
const errors = validate(input, false);
|
|
2651
|
+
const errors = [...validate(input, false)];
|
|
2652
|
+
if (errors.length === 0) errors.push(...validateRallyConfigRelations(input));
|
|
2330
2653
|
return errors.length === 0 ? { success: true, data: input } : { success: false, errors };
|
|
2331
2654
|
}
|
|
2332
2655
|
function parseAdminConfig(input) {
|
|
@@ -2335,7 +2658,8 @@ function parseAdminConfig(input) {
|
|
|
2335
2658
|
return result.data;
|
|
2336
2659
|
}
|
|
2337
2660
|
function safeParsePublicConfig(input) {
|
|
2338
|
-
const errors = validate(input, true);
|
|
2661
|
+
const errors = [...validate(input, true)];
|
|
2662
|
+
if (errors.length === 0) errors.push(...validateRallyConfigRelations(input));
|
|
2339
2663
|
return errors.length === 0 ? { success: true, data: input } : { success: false, errors };
|
|
2340
2664
|
}
|
|
2341
2665
|
function parsePublicConfig(input) {
|
|
@@ -2463,6 +2787,6 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
|
|
|
2463
2787
|
}
|
|
2464
2788
|
}
|
|
2465
2789
|
|
|
2466
|
-
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, exportProgressToken, getCurrentGeoContext, getOrderedSpots, 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 };
|
|
2790
|
+
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, validateRallyConfigRelations, verifyPasscode, verifySecureToken, verifySnapshotToken };
|
|
2467
2791
|
//# sourceMappingURL=index.js.map
|
|
2468
2792
|
//# sourceMappingURL=index.js.map
|