@stamprally/core 0.21.0 → 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.21.0
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.21.0",
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.21.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.21.0 batch sync, queue capability, 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,16 +56,18 @@ 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` also exposes `mode` (`"persistent"` or
66
- `"volatile_memory"`), `isPersistent`, and the actual `storage` type. The
67
- deprecated `legacy` property and `getLegacyQueueCapability` helper expose
68
- `"persistent"` or `"volatile"` while callers migrate to `mode`.
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"`.
69
71
 
70
72
  `evaluateSpotStatus` derives `UNCLAIMED`, `CLAIMED`, `LOCKED`, or `VERIFYING`
71
73
  without mutating state. A spot with incomplete `prerequisites` is `LOCKED` and
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) => {
@@ -1008,6 +1014,8 @@ var IndexedDBAdapter = class {
1008
1014
 
1009
1015
  // src/client/offlineQueue.ts
1010
1016
  function getLegacyQueueCapability(capability) {
1017
+ if (typeof capability === "string")
1018
+ return capability === "memory" || capability === "disabled" ? "volatile" : "persistent";
1011
1019
  return capability.isPersistent ? "persistent" : "volatile";
1012
1020
  }
1013
1021
  function rollbackOptimisticOperation(state, operation) {
@@ -1268,13 +1276,15 @@ var OfflineQueue = class {
1268
1276
  return this.#operations.length;
1269
1277
  }
1270
1278
  get queueCapability() {
1271
- const isPersistent = this.#queueCapability !== "memory";
1279
+ return this.#queueCapability;
1280
+ }
1281
+ get queueCapabilities() {
1282
+ const storageType = this.#queueCapability;
1283
+ const isPersistent = storageType !== "memory";
1272
1284
  return {
1273
- storage: this.#queueCapability,
1274
- mode: isPersistent ? "persistent" : "volatile_memory",
1275
- multiTabSync: this.#hasWebLocks() ? "supported_web_locks" : "disabled_unsafe_environment",
1285
+ storageType,
1276
1286
  isPersistent,
1277
- legacy: isPersistent ? "persistent" : "volatile"
1287
+ multiTabSync: this.#hasWebLocks() ? "supported_web_locks" : "disabled_unsafe_environment"
1278
1288
  };
1279
1289
  }
1280
1290
  get storageCapability() {
@@ -1332,7 +1342,7 @@ var OfflineQueue = class {
1332
1342
  }
1333
1343
  if (this.#queueCapability === "memory")
1334
1344
  this.#warnCapability("Offline queue persistence is unavailable; queued data is memory-only.");
1335
- if (this.queueCapability.multiTabSync === "disabled_unsafe_environment")
1345
+ if (this.queueCapabilities.multiTabSync === "disabled_unsafe_environment")
1336
1346
  this.#warnCapability(
1337
1347
  "Web Locks is unavailable; automatic cross-tab synchronization is disabled. Sync must be triggered by the foreground tab."
1338
1348
  );
@@ -1770,8 +1780,11 @@ function isReplayable(operation) {
1770
1780
  return operation.status === void 0 || operation.status === "ACCEPTED" || operation.status === "PENDING" || operation.status === "IN_FLIGHT" || operation.status === "FAILED_RETRYABLE";
1771
1781
  }
1772
1782
  function operationId(operation) {
1773
- const identity = operation.request.userId ?? operation.request.anonymousSessionId ?? "anonymous";
1774
- 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;
1775
1788
  }
1776
1789
  function applyInventoryDelta(state, previous, optimistic) {
1777
1790
  const previousInventory = previous.inventory;
@@ -1847,12 +1860,19 @@ function rebuildUserStateFromLog(baselineOrOptions, operationsArgument, configAr
1847
1860
  function rebuildUserStateLog(baseline, operations, config) {
1848
1861
  let state = cloneState(baseline);
1849
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);
1850
1870
  const rejectedCheckIns = new Set(
1851
- operations.filter(
1871
+ sortedOperations.filter(
1852
1872
  (operation) => operation.status === "REJECTED_PERMANENT" && operation.kind === "checkIn"
1853
1873
  ).map((operation) => operation.kind === "checkIn" ? operation.request.spotId : void 0).filter((spotId) => spotId !== void 0)
1854
1874
  );
1855
- for (const operation of operations) {
1875
+ for (const operation of sortedOperations) {
1856
1876
  if (operation.status === "REJECTED_PERMANENT") {
1857
1877
  if (operation.kind === "checkIn") rejectedCheckIns.add(operation.request.spotId);
1858
1878
  continue;
@@ -1960,7 +1980,7 @@ var StampRallyClient = class {
1960
1980
  this.#storage = this.#options.storage ?? new InMemoryStorage();
1961
1981
  this.#anonymousSessionId = this.#options.anonymousSessionId ?? createAnonymousSessionId();
1962
1982
  this.#userId = this.#options.userId ?? this.#anonymousSessionId;
1963
- this.#offlineQueue = this.#options.offlineQueue;
1983
+ this.#offlineQueue = this.#options.offlineQueue === false ? void 0 : this.#options.offlineQueue;
1964
1984
  this.#offlineQueue?.setReplayConfig(config);
1965
1985
  this.#offlineQueue?.setSyncResultListener((event) => this.#handleOfflineSyncResult(event));
1966
1986
  this.#offlineQueue?.setCapabilityWarningListener(
@@ -1996,19 +2016,20 @@ var StampRallyClient = class {
1996
2016
  return this.#syncRevision;
1997
2017
  }
1998
2018
  get queueCapability() {
1999
- return this.#offlineQueue?.queueCapability ?? {
2000
- storage: "custom",
2001
- mode: "persistent",
2002
- multiTabSync: "disabled_unsafe_environment",
2003
- isPersistent: true,
2004
- legacy: "persistent"
2019
+ return this.#offlineQueue?.queueCapability ?? "disabled";
2020
+ }
2021
+ get queueCapabilities() {
2022
+ return this.#offlineQueue?.queueCapabilities ?? {
2023
+ storageType: "none",
2024
+ isPersistent: false,
2025
+ multiTabSync: "disabled_unsafe_environment"
2005
2026
  };
2006
2027
  }
2007
2028
  get storageCapability() {
2008
- return this.#offlineQueue?.storageCapability ?? "custom";
2029
+ return this.#offlineQueue?.storageCapability ?? "disabled";
2009
2030
  }
2010
2031
  get isStoragePersistent() {
2011
- return this.#offlineQueue?.isStoragePersistent ?? true;
2032
+ return this.#offlineQueue?.isStoragePersistent ?? false;
2012
2033
  }
2013
2034
  discardRejected(operationId2) {
2014
2035
  return this.#offlineQueue?.discardRejected(operationId2) ?? Promise.resolve(false);
@@ -3518,6 +3539,7 @@ exports.rollbackOptimisticOperation = rollbackOptimisticOperation;
3518
3539
  exports.safeParseAdminConfig = safeParseAdminConfig;
3519
3540
  exports.safeParsePublicConfig = safeParsePublicConfig;
3520
3541
  exports.sanitizeAdminConfig = sanitizeAdminConfig;
3542
+ exports.sortOperationsDeterministically = sortOperationsDeterministically;
3521
3543
  exports.storageKey = storageKey;
3522
3544
  exports.toLocalizedString = toLocalizedString;
3523
3545
  exports.toPublicConfig = toPublicConfig;