@stamprally/core 0.18.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 CHANGED
@@ -1,4 +1,4 @@
1
- # @stamprally/core v0.18.0
1
+ # @stamprally/core v0.19.0
2
2
 
3
3
  Dependency-free domain models, immutable state transitions, storage adapters, browser detectors, and safe configuration parsers.
4
4
 
@@ -7,7 +7,7 @@ import { InMemoryStorage, StampRallyClient, type PublicRallyConfig } from "@stam
7
7
 
8
8
  const config: PublicRallyConfig = {
9
9
  id: "city-tour",
10
- version: "0.18.0",
10
+ version: "0.19.0",
11
11
  title: "City Tour",
12
12
  spots: [{ id: "station", orderIndex: 0, name: "Central Station", conditions: [{ type: "passcode" }] }],
13
13
  rewards: [],
@@ -28,8 +28,7 @@ The browser detectors `getCurrentGeoContext`, `readNfcContext`, and `readQrConte
28
28
  Configure `OfflineQueue` with `rallyId` and `userId` to persist pending work under
29
29
  `stamprally:queue:<rallyId>:<userId-or-anonymous>`. `switchUser` loads the other
30
30
  user's queue. The default `authoritative_replay` policy uses the server state as
31
- the conflict baseline; `merge` is retained only as an explicit compatibility
32
- option, and `server_wins` uses the server state unchanged. Sync adapters may return `ACCEPTED`, `REJECTED_PERMANENT`,
31
+ the conflict baseline; it is the only supported conflict policy. Sync adapters may return `ACCEPTED`, `REJECTED_PERMANENT`,
33
32
  or `RETRYABLE_ERROR`; only the first two remove an operation.
34
33
 
35
34
  When no authenticated user is supplied, the client creates a persistent UUID v4
package/dist/index.cjs CHANGED
@@ -1581,9 +1581,10 @@ var OfflineQueue = class {
1581
1581
  );
1582
1582
  if (spot2 === void 0) return false;
1583
1583
  const failedSpots = new Set(
1584
- this.#rejectedHistory.filter((entry) => entry.operation.kind === "checkIn").map(
1585
- (entry) => entry.operation.kind === "checkIn" ? entry.operation.request.spotId : void 0
1586
- ).filter((spotId) => spotId !== void 0)
1584
+ [
1585
+ ...this.#rejectedHistory.map((entry) => entry.operation),
1586
+ ...this.#operations.filter((candidate) => candidate.status === "REJECTED_PERMANENT")
1587
+ ].filter((candidate) => candidate.kind === "checkIn").map((candidate) => candidate.kind === "checkIn" ? candidate.request.spotId : void 0).filter((spotId) => spotId !== void 0)
1587
1588
  );
1588
1589
  if (!spot2.prerequisites?.some((prerequisite) => failedSpots.has(prerequisite))) return false;
1589
1590
  const error = {
@@ -1794,18 +1795,18 @@ function applyInventoryDelta(state, previous, optimistic) {
1794
1795
  if (before !== void 0 && after !== void 0)
1795
1796
  rewardRemaining[key] = Math.max(0, (currentRewards[key] ?? before) + after - before);
1796
1797
  }
1797
- return {
1798
- ...state,
1799
- inventory: {
1800
- ...currentInventory.sharedRemaining === void 0 || sharedDelta === void 0 ? {} : { sharedRemaining: Math.max(0, currentInventory.sharedRemaining + sharedDelta) },
1801
- ...Object.keys(rewardRemaining).length === 0 ? {} : { rewardRemaining }
1802
- }
1798
+ const nextInventory = {
1799
+ ...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) },
1800
+ ...Object.keys(rewardRemaining).length === 0 ? {} : { rewardRemaining }
1803
1801
  };
1802
+ return { ...state, inventory: nextInventory };
1804
1803
  }
1805
- function applyOperation(state, operation, config) {
1804
+ function applyOperation(state, operation, config, rejectedCheckIns) {
1806
1805
  if (operation.kind === "checkIn") {
1807
1806
  const spot2 = config?.spots.find((candidate) => candidate.id === operation.request.spotId);
1808
- if (spot2?.prerequisites?.some((id2) => !state.records.some((record2) => record2.stampId === id2)))
1807
+ if (spot2?.prerequisites?.some(
1808
+ (id2) => rejectedCheckIns.has(id2) || !state.records.some((record2) => record2.stampId === id2)
1809
+ ))
1809
1810
  return { state, prerequisiteFailed: true };
1810
1811
  if (state.records.some((record2) => record2.stampId === operation.request.spotId))
1811
1812
  return { state, prerequisiteFailed: false };
@@ -1852,11 +1853,21 @@ function rebuildUserStateFromLog(baselineOrOptions, operationsArgument, configAr
1852
1853
  function rebuildUserStateLog(baseline, operations, config) {
1853
1854
  let state = cloneState(baseline);
1854
1855
  const rejectedOperationIds = [];
1856
+ const rejectedCheckIns = new Set(
1857
+ operations.filter(
1858
+ (operation) => operation.status === "REJECTED_PERMANENT" && operation.kind === "checkIn"
1859
+ ).map((operation) => operation.kind === "checkIn" ? operation.request.spotId : void 0).filter((spotId) => spotId !== void 0)
1860
+ );
1855
1861
  for (const operation of operations) {
1862
+ if (operation.status === "REJECTED_PERMANENT") {
1863
+ if (operation.kind === "checkIn") rejectedCheckIns.add(operation.request.spotId);
1864
+ continue;
1865
+ }
1856
1866
  if (!isReplayable(operation)) continue;
1857
- const replay = applyOperation(state, operation, config);
1867
+ const replay = applyOperation(state, operation, config, rejectedCheckIns);
1858
1868
  if (replay.prerequisiteFailed) {
1859
1869
  rejectedOperationIds.push(operationId(operation));
1870
+ if (operation.kind === "checkIn") rejectedCheckIns.add(operation.request.spotId);
1860
1871
  continue;
1861
1872
  }
1862
1873
  state = replay.state;
@@ -1904,6 +1915,30 @@ function emptyState(config, userId, now) {
1904
1915
  updatedAt: now
1905
1916
  };
1906
1917
  }
1918
+ function applyOptimisticRewardClaim(state, rewardId, reward2, now) {
1919
+ if (reward2.claimTicketNumber === void 0 || state.inventory === void 0)
1920
+ return {
1921
+ ...state,
1922
+ rewards: state.rewards.map((item) => item.rewardId === rewardId ? reward2 : item),
1923
+ updatedAt: now
1924
+ };
1925
+ const currentInventory = state.inventory;
1926
+ const currentRewardRemaining = currentInventory.rewardRemaining?.[rewardId];
1927
+ return {
1928
+ ...state,
1929
+ rewards: state.rewards.map((item) => item.rewardId === rewardId ? reward2 : item),
1930
+ updatedAt: now,
1931
+ inventory: {
1932
+ ...currentInventory.sharedRemaining === void 0 ? {} : { sharedRemaining: Math.max(0, currentInventory.sharedRemaining - 1) },
1933
+ ...currentInventory.rewardRemaining === void 0 || currentRewardRemaining === void 0 ? {} : {
1934
+ rewardRemaining: {
1935
+ ...currentInventory.rewardRemaining,
1936
+ [rewardId]: Math.max(0, currentRewardRemaining - 1)
1937
+ }
1938
+ }
1939
+ }
1940
+ };
1941
+ }
1907
1942
  function errorMessage(error, fallback) {
1908
1943
  return error !== void 0 && "message" in error && typeof error.message === "string" ? error.message : fallback;
1909
1944
  }
@@ -1931,6 +1966,9 @@ var StampRallyClient = class {
1931
1966
  this.#offlineQueue = this.#options.offlineQueue;
1932
1967
  this.#offlineQueue?.setReplayConfig(config);
1933
1968
  this.#offlineQueue?.setSyncResultListener((event) => this.#handleOfflineSyncResult(event));
1969
+ this.#offlineQueue?.setCapabilityWarningListener(
1970
+ (warning) => this.#emitEvent({ type: "storageCapabilityWarning", warning })
1971
+ );
1934
1972
  this.#offlineQueue?.setChangeListener(() => {
1935
1973
  this.#syncRevision += 1;
1936
1974
  if (this.#state !== null) this.#emit(this.#state);
@@ -2161,23 +2199,13 @@ var StampRallyClient = class {
2161
2199
  return result.ok ? this.#commitClaim(result) : this.#fail(result.error);
2162
2200
  } catch (error) {
2163
2201
  if (this.#offlineQueue === void 0) throw error;
2164
- const next2 = {
2165
- ...current,
2166
- rewards: current.rewards.map(
2167
- (item) => item.rewardId === rewardId ? local.value : item
2168
- ),
2169
- updatedAt: now
2170
- };
2202
+ const next2 = applyOptimisticRewardClaim(current, rewardId, local.value, now);
2171
2203
  await this.#offlineQueue.enqueueClaimReward(request, next2);
2172
2204
  await this.#storage.save(next2);
2173
2205
  return this.#commitClaim({ ok: true, value: { state: next2, reward: local.value } });
2174
2206
  }
2175
2207
  }
2176
- const next = {
2177
- ...current,
2178
- rewards: current.rewards.map((item) => item.rewardId === rewardId ? local.value : item),
2179
- updatedAt: now
2180
- };
2208
+ const next = applyOptimisticRewardClaim(current, rewardId, local.value, now);
2181
2209
  await this.#storage.save(next);
2182
2210
  return this.#commitClaim({ ok: true, value: { state: next, reward: local.value } });
2183
2211
  });
@@ -3251,7 +3279,7 @@ function validateRallyConfigRelations(config) {
3251
3279
  ["stockKey", reward2.stockKey],
3252
3280
  ["secondaryStockKey", reward2.secondaryStockKey]
3253
3281
  ]) {
3254
- if (key !== void 0 && key !== "__shared__" && inventory?.[key] === void 0)
3282
+ if (key !== void 0 && (key === "__shared__" && inventory?.sharedStock === void 0 || key !== "__shared__" && inventory?.[key] === void 0))
3255
3283
  add(
3256
3284
  errors,
3257
3285
  `rewards[${index}].${field}`,
@@ -3259,13 +3287,6 @@ function validateRallyConfigRelations(config) {
3259
3287
  "missing_inventory_key"
3260
3288
  );
3261
3289
  }
3262
- if (reward2.stockKey === "__shared__" && inventory?.sharedStock === void 0)
3263
- add(
3264
- errors,
3265
- `rewards[${index}].stockKey`,
3266
- "sharedStock is not defined.",
3267
- "missing_inventory_key"
3268
- );
3269
3290
  });
3270
3291
  const visiting = /* @__PURE__ */ new Set();
3271
3292
  const visited = /* @__PURE__ */ new Set();