@stamprally/core 0.17.0 → 0.19.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 +3 -4
- package/dist/index.cjs +314 -126
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -24
- package/dist/index.d.ts +43 -24
- package/dist/index.js +313 -127
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -108,78 +108,20 @@ function issueClaimTicketNumber(reward2, currentState, options = {}) {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
// src/engine/sync.ts
|
|
111
|
-
function
|
|
112
|
-
const serverTime = Date.parse(serverTimestamp);
|
|
113
|
-
const localTime = Date.parse(localTimestamp);
|
|
114
|
-
if (!Number.isNaN(serverTime) && !Number.isNaN(localTime))
|
|
115
|
-
return serverTime >= localTime ? serverTimestamp : localTimestamp;
|
|
116
|
-
if (!Number.isNaN(serverTime)) return serverTimestamp;
|
|
117
|
-
if (!Number.isNaN(localTime)) return localTimestamp;
|
|
118
|
-
return serverTimestamp >= localTimestamp ? serverTimestamp : localTimestamp;
|
|
119
|
-
}
|
|
120
|
-
function mergeRewardStates(serverRewards, localRewards) {
|
|
121
|
-
const merged = new Map(serverRewards.map((reward2) => [reward2.rewardId, reward2]));
|
|
122
|
-
for (const localReward of localRewards) {
|
|
123
|
-
const serverReward = merged.get(localReward.rewardId);
|
|
124
|
-
if (serverReward === void 0) {
|
|
125
|
-
merged.set(localReward.rewardId, localReward);
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
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
|
-
});
|
|
150
|
-
}
|
|
151
|
-
return [...merged.values()];
|
|
152
|
-
}
|
|
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;
|
|
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
|
-
});
|
|
167
|
-
}
|
|
168
|
-
return [...merged.values()];
|
|
169
|
-
}
|
|
170
|
-
function resolveRallyStateConflict(serverState, localState, options = { policy: "merge" }) {
|
|
171
|
-
if (options.policy === "server_wins") return serverState;
|
|
172
|
-
if (options.policy === "authoritative_replay")
|
|
173
|
-
return {
|
|
174
|
-
...serverState,
|
|
175
|
-
records: serverState.records.map((record) => ({ ...record })),
|
|
176
|
-
rewards: serverState.rewards.map((reward2) => ({ ...reward2 }))
|
|
177
|
-
};
|
|
111
|
+
function resolveRallyStateConflict(serverState, _localState) {
|
|
178
112
|
return {
|
|
179
113
|
...serverState,
|
|
180
|
-
records:
|
|
181
|
-
|
|
182
|
-
|
|
114
|
+
records: serverState.records.map((record) => ({
|
|
115
|
+
...record,
|
|
116
|
+
...record.metadata === void 0 ? {} : { metadata: { ...record.metadata } }
|
|
117
|
+
})),
|
|
118
|
+
rewards: serverState.rewards.map((reward2) => ({ ...reward2 })),
|
|
119
|
+
...serverState.inventory === void 0 ? {} : {
|
|
120
|
+
inventory: {
|
|
121
|
+
...serverState.inventory,
|
|
122
|
+
...serverState.inventory.rewardRemaining === void 0 ? {} : { rewardRemaining: { ...serverState.inventory.rewardRemaining } }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
183
125
|
};
|
|
184
126
|
}
|
|
185
127
|
|
|
@@ -1272,8 +1214,6 @@ var OfflineQueue = class {
|
|
|
1272
1214
|
#configuredKey;
|
|
1273
1215
|
#rallyId;
|
|
1274
1216
|
#userId;
|
|
1275
|
-
#conflictPolicy;
|
|
1276
|
-
#onSyncConflict;
|
|
1277
1217
|
#operations = [];
|
|
1278
1218
|
#rejectedHistory = [];
|
|
1279
1219
|
#loaded = false;
|
|
@@ -1289,6 +1229,8 @@ var OfflineQueue = class {
|
|
|
1289
1229
|
#lockStorage;
|
|
1290
1230
|
#observedLocks = /* @__PURE__ */ new Map();
|
|
1291
1231
|
#warnedMemoryLock = false;
|
|
1232
|
+
#capabilityWarningListener;
|
|
1233
|
+
#replayConfig;
|
|
1292
1234
|
#storageListener;
|
|
1293
1235
|
#channel = null;
|
|
1294
1236
|
constructor(options = {}) {
|
|
@@ -1306,8 +1248,6 @@ var OfflineQueue = class {
|
|
|
1306
1248
|
this.#configuredKey = options.key;
|
|
1307
1249
|
this.#rallyId = options.rallyId;
|
|
1308
1250
|
this.#userId = options.userId ?? null;
|
|
1309
|
-
this.#conflictPolicy = options.conflictPolicy ?? "authoritative_replay";
|
|
1310
|
-
this.#onSyncConflict = options.onSyncConflict;
|
|
1311
1251
|
this.#synchronizeInstances = options.synchronizeInstances ?? true;
|
|
1312
1252
|
const retryOptions = {
|
|
1313
1253
|
...DEFAULT_RETRY_OPTIONS,
|
|
@@ -1330,6 +1270,14 @@ var OfflineQueue = class {
|
|
|
1330
1270
|
get queueCapability() {
|
|
1331
1271
|
return this.#queueCapability;
|
|
1332
1272
|
}
|
|
1273
|
+
get storageCapability() {
|
|
1274
|
+
if (this.#queueCapability === "memory") return "memory";
|
|
1275
|
+
const locks = globalThis.navigator?.locks;
|
|
1276
|
+
return locks !== void 0 || this.#lockStorage !== null ? this.#queueCapability : "volatile_single_tab";
|
|
1277
|
+
}
|
|
1278
|
+
get isStoragePersistent() {
|
|
1279
|
+
return this.#queueCapability !== "memory";
|
|
1280
|
+
}
|
|
1333
1281
|
get rejectedHistory() {
|
|
1334
1282
|
return this.#rejectedHistory;
|
|
1335
1283
|
}
|
|
@@ -1339,9 +1287,6 @@ var OfflineQueue = class {
|
|
|
1339
1287
|
get operations() {
|
|
1340
1288
|
return this.#operations;
|
|
1341
1289
|
}
|
|
1342
|
-
get conflictPolicy() {
|
|
1343
|
-
return this.#conflictPolicy;
|
|
1344
|
-
}
|
|
1345
1290
|
get storageKey() {
|
|
1346
1291
|
return this.#storageKey();
|
|
1347
1292
|
}
|
|
@@ -1357,6 +1302,12 @@ var OfflineQueue = class {
|
|
|
1357
1302
|
setChangeListener(listener) {
|
|
1358
1303
|
this.#changeListener = listener;
|
|
1359
1304
|
}
|
|
1305
|
+
setCapabilityWarningListener(listener) {
|
|
1306
|
+
this.#capabilityWarningListener = listener;
|
|
1307
|
+
}
|
|
1308
|
+
setReplayConfig(config) {
|
|
1309
|
+
this.#replayConfig = config;
|
|
1310
|
+
}
|
|
1360
1311
|
async initialize() {
|
|
1361
1312
|
if (this.#loaded) return;
|
|
1362
1313
|
try {
|
|
@@ -1373,6 +1324,8 @@ var OfflineQueue = class {
|
|
|
1373
1324
|
`Offline queue persistence is unavailable; queued data can be lost after reload (${String(error)}).`
|
|
1374
1325
|
);
|
|
1375
1326
|
}
|
|
1327
|
+
if (this.#queueCapability === "memory")
|
|
1328
|
+
this.#warnMemoryLock("Offline queue persistence is unavailable; queued data is memory-only.");
|
|
1376
1329
|
this.#loaded = true;
|
|
1377
1330
|
}
|
|
1378
1331
|
/** Releases browser listeners when the queue is no longer used. */
|
|
@@ -1421,11 +1374,19 @@ var OfflineQueue = class {
|
|
|
1421
1374
|
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1422
1375
|
this.#announceChange();
|
|
1423
1376
|
}
|
|
1424
|
-
async enqueueCheckIn(request) {
|
|
1425
|
-
return this.enqueue({
|
|
1377
|
+
async enqueueCheckIn(request, optimisticState) {
|
|
1378
|
+
return this.enqueue({
|
|
1379
|
+
kind: "checkIn",
|
|
1380
|
+
request,
|
|
1381
|
+
...optimisticState === void 0 ? {} : { optimisticState }
|
|
1382
|
+
});
|
|
1426
1383
|
}
|
|
1427
|
-
async enqueueClaimReward(request) {
|
|
1428
|
-
return this.enqueue({
|
|
1384
|
+
async enqueueClaimReward(request, optimisticState) {
|
|
1385
|
+
return this.enqueue({
|
|
1386
|
+
kind: "claimReward",
|
|
1387
|
+
request,
|
|
1388
|
+
...optimisticState === void 0 ? {} : { optimisticState }
|
|
1389
|
+
});
|
|
1429
1390
|
}
|
|
1430
1391
|
async clear() {
|
|
1431
1392
|
await this.initialize();
|
|
@@ -1433,10 +1394,10 @@ var OfflineQueue = class {
|
|
|
1433
1394
|
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1434
1395
|
this.#announceChange();
|
|
1435
1396
|
}
|
|
1436
|
-
async discardRejected(
|
|
1397
|
+
async discardRejected(operationId2) {
|
|
1437
1398
|
await this.initialize();
|
|
1438
1399
|
const next = this.#rejectedHistory.filter(
|
|
1439
|
-
(entry) => offlineOperationId(entry.operation) !==
|
|
1400
|
+
(entry) => offlineOperationId(entry.operation) !== operationId2
|
|
1440
1401
|
);
|
|
1441
1402
|
if (next.length === this.#rejectedHistory.length) return false;
|
|
1442
1403
|
this.#rejectedHistory = next;
|
|
@@ -1444,13 +1405,13 @@ var OfflineQueue = class {
|
|
|
1444
1405
|
this.#announceChange();
|
|
1445
1406
|
return true;
|
|
1446
1407
|
}
|
|
1447
|
-
async retryRejected(
|
|
1408
|
+
async retryRejected(operationId2) {
|
|
1448
1409
|
await this.initialize();
|
|
1449
1410
|
const entry = this.#rejectedHistory.find(
|
|
1450
|
-
(candidate) => offlineOperationId(candidate.operation) ===
|
|
1411
|
+
(candidate) => offlineOperationId(candidate.operation) === operationId2
|
|
1451
1412
|
);
|
|
1452
1413
|
if (entry === void 0) return false;
|
|
1453
|
-
if (!this.#operations.some((operation) => offlineOperationId(operation) ===
|
|
1414
|
+
if (!this.#operations.some((operation) => offlineOperationId(operation) === operationId2))
|
|
1454
1415
|
this.#operations = [
|
|
1455
1416
|
...this.#operations,
|
|
1456
1417
|
{ ...entry.operation, status: "PENDING", attempts: 0 }
|
|
@@ -1461,11 +1422,11 @@ var OfflineQueue = class {
|
|
|
1461
1422
|
this.#announceChange();
|
|
1462
1423
|
return true;
|
|
1463
1424
|
}
|
|
1464
|
-
async discardRejectedOperation(
|
|
1465
|
-
return this.discardRejected(
|
|
1425
|
+
async discardRejectedOperation(operationId2) {
|
|
1426
|
+
return this.discardRejected(operationId2);
|
|
1466
1427
|
}
|
|
1467
|
-
async retryRejectedOperation(
|
|
1468
|
-
return this.retryRejected(
|
|
1428
|
+
async retryRejectedOperation(operationId2) {
|
|
1429
|
+
return this.retryRejected(operationId2);
|
|
1469
1430
|
}
|
|
1470
1431
|
async clearRejectedHistory() {
|
|
1471
1432
|
await this.initialize();
|
|
@@ -1513,7 +1474,7 @@ var OfflineQueue = class {
|
|
|
1513
1474
|
if (callbackStarted) throw error;
|
|
1514
1475
|
}
|
|
1515
1476
|
}
|
|
1516
|
-
if (this
|
|
1477
|
+
if (this.storageCapability === "volatile_single_tab")
|
|
1517
1478
|
this.#warnMemoryLock(
|
|
1518
1479
|
"No cross-tab storage lock is available; offline sync is single-tab only."
|
|
1519
1480
|
);
|
|
@@ -1533,6 +1494,7 @@ var OfflineQueue = class {
|
|
|
1533
1494
|
while (this.#operations.length > 0) {
|
|
1534
1495
|
const operation = this.#operations[0];
|
|
1535
1496
|
if (operation === void 0) break;
|
|
1497
|
+
if (await this.#rejectFailedPrerequisite(operation)) continue;
|
|
1536
1498
|
let attempt = 0;
|
|
1537
1499
|
let response;
|
|
1538
1500
|
while (true) {
|
|
@@ -1610,6 +1572,45 @@ var OfflineQueue = class {
|
|
|
1610
1572
|
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1611
1573
|
this.#announceChange();
|
|
1612
1574
|
}
|
|
1575
|
+
async #rejectFailedPrerequisite(operation) {
|
|
1576
|
+
if (operation.kind !== "checkIn" || this.#replayConfig === void 0) return false;
|
|
1577
|
+
const spot2 = this.#replayConfig.spots.find(
|
|
1578
|
+
(candidate) => candidate.id === operation.request.spotId
|
|
1579
|
+
);
|
|
1580
|
+
if (spot2 === void 0) return false;
|
|
1581
|
+
const failedSpots = new Set(
|
|
1582
|
+
[
|
|
1583
|
+
...this.#rejectedHistory.map((entry) => entry.operation),
|
|
1584
|
+
...this.#operations.filter((candidate) => candidate.status === "REJECTED_PERMANENT")
|
|
1585
|
+
].filter((candidate) => candidate.kind === "checkIn").map((candidate) => candidate.kind === "checkIn" ? candidate.request.spotId : void 0).filter((spotId) => spotId !== void 0)
|
|
1586
|
+
);
|
|
1587
|
+
if (!spot2.prerequisites?.some((prerequisite) => failedSpots.has(prerequisite))) return false;
|
|
1588
|
+
const error = {
|
|
1589
|
+
code: "REJECTED_PREREQUISITE_FAILED",
|
|
1590
|
+
message: "A prerequisite operation was rejected by the server."
|
|
1591
|
+
};
|
|
1592
|
+
const rejectedOperation = { ...operation, status: "REJECTED_PERMANENT" };
|
|
1593
|
+
this.#rejectedHistory = [
|
|
1594
|
+
...this.#rejectedHistory,
|
|
1595
|
+
{
|
|
1596
|
+
operation: rejectedOperation,
|
|
1597
|
+
reason: error,
|
|
1598
|
+
errorCode: error.code,
|
|
1599
|
+
rejectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1600
|
+
attempts: operation.attempts ?? 0
|
|
1601
|
+
}
|
|
1602
|
+
];
|
|
1603
|
+
this.#operations = this.#operations.slice(1);
|
|
1604
|
+
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1605
|
+
await this.#saveRejectedHistory();
|
|
1606
|
+
this.#announceChange();
|
|
1607
|
+
await this.#syncResultListener?.({
|
|
1608
|
+
operation,
|
|
1609
|
+
status: "REJECTED_PERMANENT",
|
|
1610
|
+
error
|
|
1611
|
+
});
|
|
1612
|
+
return true;
|
|
1613
|
+
}
|
|
1613
1614
|
#subscribeToExternalChanges() {
|
|
1614
1615
|
const windowLike = globalThis.window;
|
|
1615
1616
|
if (windowLike !== void 0) {
|
|
@@ -1734,11 +1735,6 @@ var OfflineQueue = class {
|
|
|
1734
1735
|
}
|
|
1735
1736
|
return { status: "ACCEPTED", result: value };
|
|
1736
1737
|
}
|
|
1737
|
-
async resolveConflict(operation, localState, serverState) {
|
|
1738
|
-
const configured = this.#onSyncConflict;
|
|
1739
|
-
const policy = (typeof configured === "function" ? await configured({ operation, localState, serverState }) : configured) ?? this.#conflictPolicy;
|
|
1740
|
-
return resolveRallyStateConflict(serverState, localState, { policy });
|
|
1741
|
-
}
|
|
1742
1738
|
async #saveRejectedHistory() {
|
|
1743
1739
|
await this.#storage.saveRejectedHistory?.(this.#storageKey(), this.#rejectedHistory);
|
|
1744
1740
|
}
|
|
@@ -1746,6 +1742,12 @@ var OfflineQueue = class {
|
|
|
1746
1742
|
if (this.#warnedMemoryLock) return;
|
|
1747
1743
|
this.#warnedMemoryLock = true;
|
|
1748
1744
|
console.warn(`[@stamprally/core] ${message}`);
|
|
1745
|
+
this.#capabilityWarningListener?.({
|
|
1746
|
+
type: "STORAGE_CAPABILITY_WARNING",
|
|
1747
|
+
storageCapability: this.storageCapability === "memory" ? "memory" : "volatile_single_tab",
|
|
1748
|
+
isStoragePersistent: this.isStoragePersistent,
|
|
1749
|
+
message
|
|
1750
|
+
});
|
|
1749
1751
|
}
|
|
1750
1752
|
};
|
|
1751
1753
|
function normalizeOperation(operation) {
|
|
@@ -1767,6 +1769,110 @@ function normalizeRejectedHistory(entry) {
|
|
|
1767
1769
|
};
|
|
1768
1770
|
}
|
|
1769
1771
|
|
|
1772
|
+
// src/client/sync.ts
|
|
1773
|
+
function isReplayable(operation) {
|
|
1774
|
+
return operation.status === void 0 || operation.status === "ACCEPTED" || operation.status === "PENDING" || operation.status === "IN_FLIGHT" || operation.status === "FAILED_RETRYABLE";
|
|
1775
|
+
}
|
|
1776
|
+
function operationId(operation) {
|
|
1777
|
+
const identity = operation.request.userId ?? operation.request.anonymousSessionId ?? "anonymous";
|
|
1778
|
+
return `${operation.kind}:${operation.request.rallyId}:${identity}:${operation.request.idempotencyKey}`;
|
|
1779
|
+
}
|
|
1780
|
+
function applyInventoryDelta(state, previous, optimistic) {
|
|
1781
|
+
const previousInventory = previous.inventory;
|
|
1782
|
+
const optimisticInventory = optimistic.inventory;
|
|
1783
|
+
if (previousInventory === void 0 || optimisticInventory === void 0) return state;
|
|
1784
|
+
const currentInventory = state.inventory ?? {};
|
|
1785
|
+
const sharedDelta = previousInventory.sharedRemaining === void 0 || optimisticInventory.sharedRemaining === void 0 ? void 0 : optimisticInventory.sharedRemaining - previousInventory.sharedRemaining;
|
|
1786
|
+
const previousRewards = previousInventory.rewardRemaining ?? {};
|
|
1787
|
+
const optimisticRewards = optimisticInventory.rewardRemaining ?? {};
|
|
1788
|
+
const currentRewards = currentInventory.rewardRemaining ?? {};
|
|
1789
|
+
const rewardRemaining = { ...currentRewards };
|
|
1790
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(previousRewards), ...Object.keys(optimisticRewards)])) {
|
|
1791
|
+
const before = previousRewards[key];
|
|
1792
|
+
const after = optimisticRewards[key];
|
|
1793
|
+
if (before !== void 0 && after !== void 0)
|
|
1794
|
+
rewardRemaining[key] = Math.max(0, (currentRewards[key] ?? before) + after - before);
|
|
1795
|
+
}
|
|
1796
|
+
const nextInventory = {
|
|
1797
|
+
...currentInventory.sharedRemaining === void 0 || sharedDelta === void 0 ? optimisticInventory.sharedRemaining === void 0 || sharedDelta === void 0 ? {} : { sharedRemaining: Math.max(0, optimisticInventory.sharedRemaining) } : { sharedRemaining: Math.max(0, currentInventory.sharedRemaining + sharedDelta) },
|
|
1798
|
+
...Object.keys(rewardRemaining).length === 0 ? {} : { rewardRemaining }
|
|
1799
|
+
};
|
|
1800
|
+
return { ...state, inventory: nextInventory };
|
|
1801
|
+
}
|
|
1802
|
+
function applyOperation(state, operation, config, rejectedCheckIns) {
|
|
1803
|
+
if (operation.kind === "checkIn") {
|
|
1804
|
+
const spot2 = config?.spots.find((candidate) => candidate.id === operation.request.spotId);
|
|
1805
|
+
if (spot2?.prerequisites?.some(
|
|
1806
|
+
(id2) => rejectedCheckIns.has(id2) || !state.records.some((record2) => record2.stampId === id2)
|
|
1807
|
+
))
|
|
1808
|
+
return { state, prerequisiteFailed: true };
|
|
1809
|
+
if (state.records.some((record2) => record2.stampId === operation.request.spotId))
|
|
1810
|
+
return { state, prerequisiteFailed: false };
|
|
1811
|
+
const optimisticRecord = operation.optimisticState?.records.find(
|
|
1812
|
+
(record2) => record2.stampId === operation.request.spotId
|
|
1813
|
+
);
|
|
1814
|
+
const record = optimisticRecord ?? {
|
|
1815
|
+
stampId: operation.request.spotId,
|
|
1816
|
+
acquiredAt: operation.request.now
|
|
1817
|
+
};
|
|
1818
|
+
const records = [...state.records, { ...record }];
|
|
1819
|
+
const rewards2 = config === void 0 ? state.rewards : reconcileRewardStates(config.rewards, state.rewards, records.length, record.acquiredAt);
|
|
1820
|
+
return {
|
|
1821
|
+
state: { ...state, records, rewards: rewards2, updatedAt: record.acquiredAt },
|
|
1822
|
+
prerequisiteFailed: false
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
const optimisticState = operation.optimisticState;
|
|
1826
|
+
if (optimisticState === void 0) return { state, prerequisiteFailed: false };
|
|
1827
|
+
const optimisticReward = optimisticState?.rewards.find(
|
|
1828
|
+
(reward2) => reward2.rewardId === operation.request.rewardId
|
|
1829
|
+
);
|
|
1830
|
+
if (optimisticReward === void 0) return { state, prerequisiteFailed: false };
|
|
1831
|
+
const rewards = state.rewards.some((reward2) => reward2.rewardId === optimisticReward.rewardId) ? state.rewards.map(
|
|
1832
|
+
(reward2) => reward2.rewardId === optimisticReward.rewardId ? { ...optimisticReward } : reward2
|
|
1833
|
+
) : [...state.rewards, { ...optimisticReward }];
|
|
1834
|
+
return {
|
|
1835
|
+
state: applyInventoryDelta(
|
|
1836
|
+
{ ...state, rewards, updatedAt: optimisticReward.consumedAt ?? state.updatedAt },
|
|
1837
|
+
operation.request.state,
|
|
1838
|
+
optimisticState
|
|
1839
|
+
),
|
|
1840
|
+
prerequisiteFailed: false
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
function rebuildUserStateFromLog(baselineOrOptions, operationsArgument, configArgument) {
|
|
1844
|
+
const { state } = rebuildUserStateLog(
|
|
1845
|
+
"baseline" in baselineOrOptions ? baselineOrOptions.baseline : baselineOrOptions,
|
|
1846
|
+
"baseline" in baselineOrOptions ? baselineOrOptions.operations : operationsArgument ?? [],
|
|
1847
|
+
"baseline" in baselineOrOptions ? baselineOrOptions.config : configArgument
|
|
1848
|
+
);
|
|
1849
|
+
return state;
|
|
1850
|
+
}
|
|
1851
|
+
function rebuildUserStateLog(baseline, operations, config) {
|
|
1852
|
+
let state = cloneState(baseline);
|
|
1853
|
+
const rejectedOperationIds = [];
|
|
1854
|
+
const rejectedCheckIns = new Set(
|
|
1855
|
+
operations.filter(
|
|
1856
|
+
(operation) => operation.status === "REJECTED_PERMANENT" && operation.kind === "checkIn"
|
|
1857
|
+
).map((operation) => operation.kind === "checkIn" ? operation.request.spotId : void 0).filter((spotId) => spotId !== void 0)
|
|
1858
|
+
);
|
|
1859
|
+
for (const operation of operations) {
|
|
1860
|
+
if (operation.status === "REJECTED_PERMANENT") {
|
|
1861
|
+
if (operation.kind === "checkIn") rejectedCheckIns.add(operation.request.spotId);
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1864
|
+
if (!isReplayable(operation)) continue;
|
|
1865
|
+
const replay = applyOperation(state, operation, config, rejectedCheckIns);
|
|
1866
|
+
if (replay.prerequisiteFailed) {
|
|
1867
|
+
rejectedOperationIds.push(operationId(operation));
|
|
1868
|
+
if (operation.kind === "checkIn") rejectedCheckIns.add(operation.request.spotId);
|
|
1869
|
+
continue;
|
|
1870
|
+
}
|
|
1871
|
+
state = replay.state;
|
|
1872
|
+
}
|
|
1873
|
+
return { state, rejectedOperationIds };
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1770
1876
|
// src/client/client.ts
|
|
1771
1877
|
function isStorage(value) {
|
|
1772
1878
|
return "load" in value && "save" in value && "remove" in value;
|
|
@@ -1807,6 +1913,30 @@ function emptyState(config, userId, now) {
|
|
|
1807
1913
|
updatedAt: now
|
|
1808
1914
|
};
|
|
1809
1915
|
}
|
|
1916
|
+
function applyOptimisticRewardClaim(state, rewardId, reward2, now) {
|
|
1917
|
+
if (reward2.claimTicketNumber === void 0 || state.inventory === void 0)
|
|
1918
|
+
return {
|
|
1919
|
+
...state,
|
|
1920
|
+
rewards: state.rewards.map((item) => item.rewardId === rewardId ? reward2 : item),
|
|
1921
|
+
updatedAt: now
|
|
1922
|
+
};
|
|
1923
|
+
const currentInventory = state.inventory;
|
|
1924
|
+
const currentRewardRemaining = currentInventory.rewardRemaining?.[rewardId];
|
|
1925
|
+
return {
|
|
1926
|
+
...state,
|
|
1927
|
+
rewards: state.rewards.map((item) => item.rewardId === rewardId ? reward2 : item),
|
|
1928
|
+
updatedAt: now,
|
|
1929
|
+
inventory: {
|
|
1930
|
+
...currentInventory.sharedRemaining === void 0 ? {} : { sharedRemaining: Math.max(0, currentInventory.sharedRemaining - 1) },
|
|
1931
|
+
...currentInventory.rewardRemaining === void 0 || currentRewardRemaining === void 0 ? {} : {
|
|
1932
|
+
rewardRemaining: {
|
|
1933
|
+
...currentInventory.rewardRemaining,
|
|
1934
|
+
[rewardId]: Math.max(0, currentRewardRemaining - 1)
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1810
1940
|
function errorMessage(error, fallback) {
|
|
1811
1941
|
return error !== void 0 && "message" in error && typeof error.message === "string" ? error.message : fallback;
|
|
1812
1942
|
}
|
|
@@ -1832,7 +1962,11 @@ var StampRallyClient = class {
|
|
|
1832
1962
|
this.#anonymousSessionId = this.#options.anonymousSessionId ?? createAnonymousSessionId();
|
|
1833
1963
|
this.#userId = this.#options.userId ?? this.#anonymousSessionId;
|
|
1834
1964
|
this.#offlineQueue = this.#options.offlineQueue;
|
|
1965
|
+
this.#offlineQueue?.setReplayConfig(config);
|
|
1835
1966
|
this.#offlineQueue?.setSyncResultListener((event) => this.#handleOfflineSyncResult(event));
|
|
1967
|
+
this.#offlineQueue?.setCapabilityWarningListener(
|
|
1968
|
+
(warning) => this.#emitEvent({ type: "storageCapabilityWarning", warning })
|
|
1969
|
+
);
|
|
1836
1970
|
this.#offlineQueue?.setChangeListener(() => {
|
|
1837
1971
|
this.#syncRevision += 1;
|
|
1838
1972
|
if (this.#state !== null) this.#emit(this.#state);
|
|
@@ -1865,17 +1999,23 @@ var StampRallyClient = class {
|
|
|
1865
1999
|
get queueCapability() {
|
|
1866
2000
|
return this.#offlineQueue?.queueCapability ?? "custom";
|
|
1867
2001
|
}
|
|
1868
|
-
|
|
1869
|
-
return this.#offlineQueue?.
|
|
2002
|
+
get storageCapability() {
|
|
2003
|
+
return this.#offlineQueue?.storageCapability ?? "custom";
|
|
2004
|
+
}
|
|
2005
|
+
get isStoragePersistent() {
|
|
2006
|
+
return this.#offlineQueue?.isStoragePersistent ?? true;
|
|
2007
|
+
}
|
|
2008
|
+
discardRejected(operationId2) {
|
|
2009
|
+
return this.#offlineQueue?.discardRejected(operationId2) ?? Promise.resolve(false);
|
|
1870
2010
|
}
|
|
1871
|
-
retryRejected(
|
|
1872
|
-
return this.#offlineQueue?.retryRejected(
|
|
2011
|
+
retryRejected(operationId2) {
|
|
2012
|
+
return this.#offlineQueue?.retryRejected(operationId2) ?? Promise.resolve(false);
|
|
1873
2013
|
}
|
|
1874
|
-
dismissRejectedOperation(
|
|
1875
|
-
return this.discardRejected(
|
|
2014
|
+
dismissRejectedOperation(operationId2) {
|
|
2015
|
+
return this.discardRejected(operationId2);
|
|
1876
2016
|
}
|
|
1877
|
-
retryOperation(
|
|
1878
|
-
return this.retryRejected(
|
|
2017
|
+
retryOperation(operationId2) {
|
|
2018
|
+
return this.retryRejected(operationId2);
|
|
1879
2019
|
}
|
|
1880
2020
|
subscribe(listener) {
|
|
1881
2021
|
this.#listeners.add(listener);
|
|
@@ -2000,13 +2140,13 @@ var StampRallyClient = class {
|
|
|
2000
2140
|
return result.ok ? this.#commitCheckIn(result) : this.#fail(result.error);
|
|
2001
2141
|
} catch (error) {
|
|
2002
2142
|
if (this.#offlineQueue === void 0) throw error;
|
|
2003
|
-
await this.#offlineQueue.enqueueCheckIn(request);
|
|
2004
2143
|
const record2 = { stampId: spotId, acquiredAt: now };
|
|
2005
2144
|
const next2 = this.#reconcile({
|
|
2006
2145
|
...current,
|
|
2007
2146
|
records: [...current.records, record2],
|
|
2008
2147
|
updatedAt: now
|
|
2009
2148
|
});
|
|
2149
|
+
await this.#offlineQueue.enqueueCheckIn(request, next2);
|
|
2010
2150
|
await this.#storage.save(next2);
|
|
2011
2151
|
return this.#commitCheckIn({ ok: true, value: { state: next2, record: record2 } });
|
|
2012
2152
|
}
|
|
@@ -2057,23 +2197,13 @@ var StampRallyClient = class {
|
|
|
2057
2197
|
return result.ok ? this.#commitClaim(result) : this.#fail(result.error);
|
|
2058
2198
|
} catch (error) {
|
|
2059
2199
|
if (this.#offlineQueue === void 0) throw error;
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
...current,
|
|
2063
|
-
rewards: current.rewards.map(
|
|
2064
|
-
(item) => item.rewardId === rewardId ? local.value : item
|
|
2065
|
-
),
|
|
2066
|
-
updatedAt: now
|
|
2067
|
-
};
|
|
2200
|
+
const next2 = applyOptimisticRewardClaim(current, rewardId, local.value, now);
|
|
2201
|
+
await this.#offlineQueue.enqueueClaimReward(request, next2);
|
|
2068
2202
|
await this.#storage.save(next2);
|
|
2069
2203
|
return this.#commitClaim({ ok: true, value: { state: next2, reward: local.value } });
|
|
2070
2204
|
}
|
|
2071
2205
|
}
|
|
2072
|
-
const next =
|
|
2073
|
-
...current,
|
|
2074
|
-
rewards: current.rewards.map((item) => item.rewardId === rewardId ? local.value : item),
|
|
2075
|
-
updatedAt: now
|
|
2076
|
-
};
|
|
2206
|
+
const next = applyOptimisticRewardClaim(current, rewardId, local.value, now);
|
|
2077
2207
|
await this.#storage.save(next);
|
|
2078
2208
|
return this.#commitClaim({ ok: true, value: { state: next, reward: local.value } });
|
|
2079
2209
|
});
|
|
@@ -2107,9 +2237,11 @@ var StampRallyClient = class {
|
|
|
2107
2237
|
state: localState,
|
|
2108
2238
|
...this.#userId === this.#anonymousSessionId ? { anonymousSessionId: this.#anonymousSessionId } : {}
|
|
2109
2239
|
});
|
|
2110
|
-
const resolved =
|
|
2111
|
-
|
|
2112
|
-
|
|
2240
|
+
const resolved = rebuildUserStateFromLog(
|
|
2241
|
+
serverState,
|
|
2242
|
+
this.#offlineQueue?.operations ?? [],
|
|
2243
|
+
this.#config
|
|
2244
|
+
);
|
|
2113
2245
|
const next = this.#reconcile(resolved);
|
|
2114
2246
|
await this.#storage.save(next);
|
|
2115
2247
|
this.#state = next;
|
|
@@ -2183,7 +2315,12 @@ var StampRallyClient = class {
|
|
|
2183
2315
|
if (event.status === "ACCEPTED") {
|
|
2184
2316
|
if (this.#syncMetrics !== null) this.#syncMetrics.processed += 1;
|
|
2185
2317
|
if (event.state !== void 0) {
|
|
2186
|
-
const
|
|
2318
|
+
const rebuilt = rebuildUserStateFromLog(
|
|
2319
|
+
event.state,
|
|
2320
|
+
this.#offlineQueue?.operations ?? [],
|
|
2321
|
+
this.#config
|
|
2322
|
+
);
|
|
2323
|
+
const next = this.#reconcile(rebuilt);
|
|
2187
2324
|
await this.#storage.save(next);
|
|
2188
2325
|
this.#state = next;
|
|
2189
2326
|
this.#emit(next);
|
|
@@ -2203,7 +2340,13 @@ var StampRallyClient = class {
|
|
|
2203
2340
|
this.#syncMetrics.failed += 1;
|
|
2204
2341
|
}
|
|
2205
2342
|
const base = event.state ?? this.#state ?? event.operation.request.state;
|
|
2206
|
-
const
|
|
2343
|
+
const rollbackBase = event.state === void 0 ? rollbackOptimisticOperation(base, event.operation) : base;
|
|
2344
|
+
const rebuilt = rebuildUserStateFromLog(
|
|
2345
|
+
rollbackBase,
|
|
2346
|
+
this.#offlineQueue?.operations ?? [],
|
|
2347
|
+
this.#config
|
|
2348
|
+
);
|
|
2349
|
+
const next = this.#reconcile(rebuilt);
|
|
2207
2350
|
await this.#storage.save(next);
|
|
2208
2351
|
this.#state = next;
|
|
2209
2352
|
this.#emit(next);
|
|
@@ -2806,7 +2949,11 @@ function theme(value, path, errors) {
|
|
|
2806
2949
|
finiteNumber(value, "gridColumns", path, errors, 1);
|
|
2807
2950
|
if (typeof value.gridColumns === "number" && !Number.isInteger(value.gridColumns))
|
|
2808
2951
|
add(errors, `${path}.gridColumns`, "Expected an integer.", "invalid_integer");
|
|
2809
|
-
if (hasOwn(value, "unclaimedOpacity"))
|
|
2952
|
+
if (hasOwn(value, "unclaimedOpacity")) {
|
|
2953
|
+
finiteNumber(value, "unclaimedOpacity", path, errors, 0);
|
|
2954
|
+
if (typeof value.unclaimedOpacity === "number" && value.unclaimedOpacity > 1)
|
|
2955
|
+
add(errors, `${path}.unclaimedOpacity`, "Expected a value between 0 and 1.", "out_of_range");
|
|
2956
|
+
}
|
|
2810
2957
|
}
|
|
2811
2958
|
function externalReferences(value, path, errors) {
|
|
2812
2959
|
if (!Array.isArray(value)) {
|
|
@@ -2969,12 +3116,14 @@ function reward(value, path, errors, isPublic) {
|
|
|
2969
3116
|
String(value.redemptionMethod)
|
|
2970
3117
|
))
|
|
2971
3118
|
add(errors, `${path}.redemptionMethod`, "Unknown redemption method.", "invalid_enum");
|
|
2972
|
-
|
|
3119
|
+
nonNegativeInteger(value, "requiredStampCount", path, errors);
|
|
2973
3120
|
for (const key of ["stockLimit", "userClaimLimit"]) {
|
|
2974
3121
|
if (hasOwn(value, key) && value[key] !== void 0) {
|
|
2975
3122
|
nonNegativeInteger(value, key, path, errors);
|
|
2976
3123
|
}
|
|
2977
3124
|
}
|
|
3125
|
+
optionalString(value, "stockKey", path, errors);
|
|
3126
|
+
optionalString(value, "secondaryStockKey", path, errors);
|
|
2978
3127
|
optionalString(value, "validUntil", path, errors);
|
|
2979
3128
|
if (typeof value.validUntil === "string" && Number.isNaN(Date.parse(value.validUntil)))
|
|
2980
3129
|
add(errors, `${path}.validUntil`, "Expected a valid date string.", "invalid_date");
|
|
@@ -3022,8 +3171,25 @@ function validate(value, isPublic) {
|
|
|
3022
3171
|
if (!isRecord3(value.inventory))
|
|
3023
3172
|
add(errors, "$.inventory", "Expected an object.", "invalid_type");
|
|
3024
3173
|
else {
|
|
3025
|
-
for (const [key, item] of Object.entries(value.inventory))
|
|
3174
|
+
for (const [key, item] of Object.entries(value.inventory)) {
|
|
3175
|
+
if (key === "global") {
|
|
3176
|
+
add(
|
|
3177
|
+
errors,
|
|
3178
|
+
"$.inventory.global",
|
|
3179
|
+
"Use sharedStock instead of global.",
|
|
3180
|
+
"deprecated_field"
|
|
3181
|
+
);
|
|
3182
|
+
continue;
|
|
3183
|
+
}
|
|
3026
3184
|
if (item !== void 0) nonNegativeInteger(value.inventory, key, "$.inventory", errors);
|
|
3185
|
+
}
|
|
3186
|
+
if (hasOwn(value.inventory, "sharedStock") && hasOwn(value.inventory, "global"))
|
|
3187
|
+
add(
|
|
3188
|
+
errors,
|
|
3189
|
+
"$.inventory",
|
|
3190
|
+
"sharedStock and global cannot be configured together.",
|
|
3191
|
+
"conflicting_fields"
|
|
3192
|
+
);
|
|
3027
3193
|
}
|
|
3028
3194
|
}
|
|
3029
3195
|
if (hasOwn(value, "inventoryMode") && value.inventoryMode !== void 0 && value.inventoryMode !== "shared" && value.inventoryMode !== "per_reward")
|
|
@@ -3099,6 +3265,26 @@ function validateRallyConfigRelations(config) {
|
|
|
3099
3265
|
reward2.conditions?.forEach((condition2, conditionIndex) => {
|
|
3100
3266
|
visit(condition2, `rewards[${index}].conditions[${conditionIndex}]`);
|
|
3101
3267
|
});
|
|
3268
|
+
if (reward2.requiredStampCount > config.spots.length)
|
|
3269
|
+
add(
|
|
3270
|
+
errors,
|
|
3271
|
+
`rewards[${index}].requiredStampCount`,
|
|
3272
|
+
"requiredStampCount cannot exceed the number of spots.",
|
|
3273
|
+
"required_stamp_count_exceeds_spots"
|
|
3274
|
+
);
|
|
3275
|
+
const inventory = "inventory" in config ? config.inventory : void 0;
|
|
3276
|
+
for (const [field, key] of [
|
|
3277
|
+
["stockKey", reward2.stockKey],
|
|
3278
|
+
["secondaryStockKey", reward2.secondaryStockKey]
|
|
3279
|
+
]) {
|
|
3280
|
+
if (key !== void 0 && (key === "__shared__" && inventory?.sharedStock === void 0 || key !== "__shared__" && inventory?.[key] === void 0))
|
|
3281
|
+
add(
|
|
3282
|
+
errors,
|
|
3283
|
+
`rewards[${index}].${field}`,
|
|
3284
|
+
"Referenced inventory key does not exist.",
|
|
3285
|
+
"missing_inventory_key"
|
|
3286
|
+
);
|
|
3287
|
+
}
|
|
3102
3288
|
});
|
|
3103
3289
|
const visiting = /* @__PURE__ */ new Set();
|
|
3104
3290
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -3269,6 +3455,6 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
|
|
|
3269
3455
|
}
|
|
3270
3456
|
}
|
|
3271
3457
|
|
|
3272
|
-
export { ConfigValidationError, DEFAULT_SHEET_THEME, MemoryQueueStorage as InMemoryOfflineQueueStorage, InMemoryStorage, IndexedDBAdapter, IndexedDBOfflineQueueStorage, LocalStorageAdapter, OfflineQueue, StampRallyClient, StorageAdapterError, THEME_PRESETS, assertPublicConfig, calculateDistanceMeters, calculateProgress, consumeReward, createAnonymousSessionId, createClaimTicketNumber, createSecureToken, createSignedSnapshotToken, createUniqueClaimTicketNumber, evaluateCondition, evaluateConditionDetailed, evaluateSpotStatus, exportProgressToken, getCurrentGeoContext, getOrderedSpots, getSpotStatus, importProgressToken, isGeolocationSupported, isNfcSupported, isPublicConfig, isQrSupported, isRewardState, isStampRallyState, issueClaimTicketNumber, normalizePasscode, offlineOperationId, parseAdminConfig, parsePublicConfig, processStamp, readNfcContext, readQrContext, reconcileRewardStates, resolveLocalizedText, resolveRallyStateConflict, rollbackOptimisticOperation, safeParseAdminConfig, safeParsePublicConfig, sanitizeAdminConfig, storageKey, toLocalizedString, toPublicConfig, updateLocalizedField, validatePublicConfigSafety, validateRallyConfigRelations, verifyPasscode, verifySecureToken, verifySnapshotToken };
|
|
3458
|
+
export { ConfigValidationError, DEFAULT_SHEET_THEME, MemoryQueueStorage as InMemoryOfflineQueueStorage, InMemoryStorage, IndexedDBAdapter, IndexedDBOfflineQueueStorage, LocalStorageAdapter, OfflineQueue, StampRallyClient, StorageAdapterError, THEME_PRESETS, assertPublicConfig, calculateDistanceMeters, calculateProgress, consumeReward, createAnonymousSessionId, createClaimTicketNumber, createSecureToken, createSignedSnapshotToken, createUniqueClaimTicketNumber, evaluateCondition, evaluateConditionDetailed, evaluateSpotStatus, exportProgressToken, getCurrentGeoContext, getOrderedSpots, getSpotStatus, importProgressToken, isGeolocationSupported, isNfcSupported, isPublicConfig, isQrSupported, isRewardState, isStampRallyState, issueClaimTicketNumber, normalizePasscode, offlineOperationId, parseAdminConfig, parsePublicConfig, processStamp, readNfcContext, readQrContext, rebuildUserStateFromLog, rebuildUserStateLog, reconcileRewardStates, resolveLocalizedText, resolveRallyStateConflict, rollbackOptimisticOperation, safeParseAdminConfig, safeParsePublicConfig, sanitizeAdminConfig, storageKey, toLocalizedString, toPublicConfig, updateLocalizedField, validatePublicConfigSafety, validateRallyConfigRelations, verifyPasscode, verifySecureToken, verifySnapshotToken };
|
|
3273
3459
|
//# sourceMappingURL=index.js.map
|
|
3274
3460
|
//# sourceMappingURL=index.js.map
|