@stamprally/core 0.20.1 → 0.22.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.20.1
1
+ # @stamprally/core v0.22.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.20.1",
10
+ version: "0.22.0",
11
11
  title: "City Tour",
12
12
  spots: [{ id: "station", orderIndex: 0, name: "Central Station", conditions: [{ type: "passcode" }] }],
13
13
  rewards: [],
@@ -23,7 +23,7 @@ The browser detectors `getCurrentGeoContext`, `readNfcContext`, and `readQrConte
23
23
 
24
24
  `InMemoryStorage`, `LocalStorageAdapter`, and `IndexedDBAdapter` implement `StampStorage`. `updateLocalizedField` updates one locale without dropping existing translations.
25
25
 
26
- ## v0.17.0 offline synchronization
26
+ ## v0.22.0 offline synchronization
27
27
 
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
@@ -35,7 +35,7 @@ When no authenticated user is supplied, the client creates a persistent UUID v4
35
35
  `anonymousSessionId` and includes it in sync requests. Queue replay uses Web Locks
36
36
  when available and supports bounded exponential backoff through `retryOptions`.
37
37
 
38
- ### v0.20.1 batch sync and storage capability
38
+ ### v0.22.0 batch sync, queue capability, and storage capability
39
39
 
40
40
  An adapter can return `SyncProgressResponse` from `sync` when it sends queued
41
41
  operations to a server-side `syncProgress` endpoint. The adapter passes queued
@@ -56,12 +56,19 @@ results are removed from the queue; permanent reasons are added to
56
56
  permanent prerequisite failure does not prevent later independent operations from
57
57
  being evaluated.
58
58
 
59
- `client.queueCapability.multiTabSync` is `"supported_web_locks"` when the Web
59
+ `client.queueCapabilities.multiTabSync` is `"supported_web_locks"` when the Web
60
60
  Locks API is available. Otherwise it is `"disabled_unsafe_environment"`:
61
61
  cross-tab automatic synchronization is disabled, the foreground tab must trigger
62
62
  sync explicitly, and a `storageCapabilityWarning` event is emitted. localStorage
63
63
  may still be used for durable queue data, but it is never used as a lock.
64
64
 
65
+ `client.queueCapability` remains the legacy storage string (`"indexeddb"`,
66
+ `"localstorage"`, `"memory"`, `"custom"`, or `"disabled"`) so string comparisons
67
+ such as `client.queueCapability === "indexeddb"` remain valid. New code can use
68
+ `client.queueCapabilities`, which provides `storageType`, `isPersistent`, and
69
+ `multiTabSync`. An omitted or `false` `offlineQueue` is reported as
70
+ `disabled` with `storageType: "none"`.
71
+
65
72
  `evaluateSpotStatus` derives `UNCLAIMED`, `CLAIMED`, `LOCKED`, or `VERIFYING`
66
73
  without mutating state. A spot with incomplete `prerequisites` is `LOCKED` and
67
74
  must not be verified by a client or viewer.
package/dist/index.cjs CHANGED
@@ -457,6 +457,12 @@ async function readQrContext(videoElement, options = {}) {
457
457
  }
458
458
 
459
459
  // src/engine/transition.ts
460
+ function sortOperationsDeterministically(operations) {
461
+ return [...operations].sort((left, right) => {
462
+ if (left.timestamp !== right.timestamp) return left.timestamp - right.timestamp;
463
+ return left.operationId.localeCompare(right.operationId);
464
+ });
465
+ }
460
466
  function reconcileRewardStates(rewards, currentStates, acquiredStampCount, now) {
461
467
  const states = new Map(currentStates.map((state) => [state.rewardId, state]));
462
468
  return rewards.map((reward2) => {
@@ -1007,6 +1013,11 @@ var IndexedDBAdapter = class {
1007
1013
  };
1008
1014
 
1009
1015
  // src/client/offlineQueue.ts
1016
+ function getLegacyQueueCapability(capability) {
1017
+ if (typeof capability === "string")
1018
+ return capability === "memory" || capability === "disabled" ? "volatile" : "persistent";
1019
+ return capability.isPersistent ? "persistent" : "volatile";
1020
+ }
1010
1021
  function rollbackOptimisticOperation(state, operation) {
1011
1022
  const previous = operation.request.state;
1012
1023
  if (operation.kind === "checkIn") {
@@ -1265,8 +1276,14 @@ var OfflineQueue = class {
1265
1276
  return this.#operations.length;
1266
1277
  }
1267
1278
  get queueCapability() {
1279
+ return this.#queueCapability;
1280
+ }
1281
+ get queueCapabilities() {
1282
+ const storageType = this.#queueCapability;
1283
+ const isPersistent = storageType !== "memory";
1268
1284
  return {
1269
- storage: this.#queueCapability,
1285
+ storageType,
1286
+ isPersistent,
1270
1287
  multiTabSync: this.#hasWebLocks() ? "supported_web_locks" : "disabled_unsafe_environment"
1271
1288
  };
1272
1289
  }
@@ -1325,7 +1342,7 @@ var OfflineQueue = class {
1325
1342
  }
1326
1343
  if (this.#queueCapability === "memory")
1327
1344
  this.#warnCapability("Offline queue persistence is unavailable; queued data is memory-only.");
1328
- if (this.queueCapability.multiTabSync === "disabled_unsafe_environment")
1345
+ if (this.queueCapabilities.multiTabSync === "disabled_unsafe_environment")
1329
1346
  this.#warnCapability(
1330
1347
  "Web Locks is unavailable; automatic cross-tab synchronization is disabled. Sync must be triggered by the foreground tab."
1331
1348
  );
@@ -1763,8 +1780,11 @@ function isReplayable(operation) {
1763
1780
  return operation.status === void 0 || operation.status === "ACCEPTED" || operation.status === "PENDING" || operation.status === "IN_FLIGHT" || operation.status === "FAILED_RETRYABLE";
1764
1781
  }
1765
1782
  function operationId(operation) {
1766
- const identity = operation.request.userId ?? operation.request.anonymousSessionId ?? "anonymous";
1767
- return `${operation.kind}:${operation.request.rallyId}:${identity}:${operation.request.idempotencyKey}`;
1783
+ return offlineOperationId(operation);
1784
+ }
1785
+ function operationTimestamp(operation) {
1786
+ const parsed = Date.parse(operation.request.now);
1787
+ return Number.isFinite(parsed) ? parsed : Number.MAX_SAFE_INTEGER;
1768
1788
  }
1769
1789
  function applyInventoryDelta(state, previous, optimistic) {
1770
1790
  const previousInventory = previous.inventory;
@@ -1840,12 +1860,19 @@ function rebuildUserStateFromLog(baselineOrOptions, operationsArgument, configAr
1840
1860
  function rebuildUserStateLog(baseline, operations, config) {
1841
1861
  let state = cloneState(baseline);
1842
1862
  const rejectedOperationIds = [];
1863
+ const sortedOperations = sortOperationsDeterministically(
1864
+ operations.map((operation) => ({
1865
+ operation,
1866
+ operationId: offlineOperationId(operation),
1867
+ timestamp: operationTimestamp(operation)
1868
+ }))
1869
+ ).map(({ operation }) => operation);
1843
1870
  const rejectedCheckIns = new Set(
1844
- operations.filter(
1871
+ sortedOperations.filter(
1845
1872
  (operation) => operation.status === "REJECTED_PERMANENT" && operation.kind === "checkIn"
1846
1873
  ).map((operation) => operation.kind === "checkIn" ? operation.request.spotId : void 0).filter((spotId) => spotId !== void 0)
1847
1874
  );
1848
- for (const operation of operations) {
1875
+ for (const operation of sortedOperations) {
1849
1876
  if (operation.status === "REJECTED_PERMANENT") {
1850
1877
  if (operation.kind === "checkIn") rejectedCheckIns.add(operation.request.spotId);
1851
1878
  continue;
@@ -1953,7 +1980,7 @@ var StampRallyClient = class {
1953
1980
  this.#storage = this.#options.storage ?? new InMemoryStorage();
1954
1981
  this.#anonymousSessionId = this.#options.anonymousSessionId ?? createAnonymousSessionId();
1955
1982
  this.#userId = this.#options.userId ?? this.#anonymousSessionId;
1956
- this.#offlineQueue = this.#options.offlineQueue;
1983
+ this.#offlineQueue = this.#options.offlineQueue === false ? void 0 : this.#options.offlineQueue;
1957
1984
  this.#offlineQueue?.setReplayConfig(config);
1958
1985
  this.#offlineQueue?.setSyncResultListener((event) => this.#handleOfflineSyncResult(event));
1959
1986
  this.#offlineQueue?.setCapabilityWarningListener(
@@ -1989,16 +2016,20 @@ var StampRallyClient = class {
1989
2016
  return this.#syncRevision;
1990
2017
  }
1991
2018
  get queueCapability() {
1992
- return this.#offlineQueue?.queueCapability ?? {
1993
- storage: "custom",
2019
+ return this.#offlineQueue?.queueCapability ?? "disabled";
2020
+ }
2021
+ get queueCapabilities() {
2022
+ return this.#offlineQueue?.queueCapabilities ?? {
2023
+ storageType: "none",
2024
+ isPersistent: false,
1994
2025
  multiTabSync: "disabled_unsafe_environment"
1995
2026
  };
1996
2027
  }
1997
2028
  get storageCapability() {
1998
- return this.#offlineQueue?.storageCapability ?? "custom";
2029
+ return this.#offlineQueue?.storageCapability ?? "disabled";
1999
2030
  }
2000
2031
  get isStoragePersistent() {
2001
- return this.#offlineQueue?.isStoragePersistent ?? true;
2032
+ return this.#offlineQueue?.isStoragePersistent ?? false;
2002
2033
  }
2003
2034
  discardRejected(operationId2) {
2004
2035
  return this.#offlineQueue?.discardRejected(operationId2) ?? Promise.resolve(false);
@@ -3481,6 +3512,7 @@ exports.evaluateConditionDetailed = evaluateConditionDetailed;
3481
3512
  exports.evaluateSpotStatus = evaluateSpotStatus;
3482
3513
  exports.exportProgressToken = exportProgressToken;
3483
3514
  exports.getCurrentGeoContext = getCurrentGeoContext;
3515
+ exports.getLegacyQueueCapability = getLegacyQueueCapability;
3484
3516
  exports.getOrderedSpots = getOrderedSpots;
3485
3517
  exports.getSpotStatus = getSpotStatus;
3486
3518
  exports.importProgressToken = importProgressToken;
@@ -3507,6 +3539,7 @@ exports.rollbackOptimisticOperation = rollbackOptimisticOperation;
3507
3539
  exports.safeParseAdminConfig = safeParseAdminConfig;
3508
3540
  exports.safeParsePublicConfig = safeParsePublicConfig;
3509
3541
  exports.sanitizeAdminConfig = sanitizeAdminConfig;
3542
+ exports.sortOperationsDeterministically = sortOperationsDeterministically;
3510
3543
  exports.storageKey = storageKey;
3511
3544
  exports.toLocalizedString = toLocalizedString;
3512
3545
  exports.toPublicConfig = toPublicConfig;