@stamprally/core 0.12.0 → 0.13.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/dist/index.d.cts CHANGED
@@ -147,6 +147,7 @@ interface StampRecord {
147
147
  readonly acquiredAt: string;
148
148
  readonly metadata?: Readonly<Record<string, unknown>>;
149
149
  }
150
+ type SpotStatus = "UNCLAIMED" | "CLAIMED" | "LOCKED" | "VERIFYING";
150
151
  type RewardStatus = "LOCKED" | "AVAILABLE" | "CONSUMED" | "EXPIRED";
151
152
  interface RewardState {
152
153
  readonly rewardId: string;
@@ -292,6 +293,11 @@ declare function parsePublicConfig(input: unknown): PublicRallyConfig;
292
293
  declare function calculateDistanceMeters(aLat: number, aLon: number, bLat: number, bLon: number): number;
293
294
  declare function evaluateConditionDetailed(condition: CheckInCondition, context: VerificationContext): Result<ConditionMatch, ConditionMismatch>;
294
295
  declare function evaluateCondition(condition: CheckInCondition, context: VerificationContext): boolean;
296
+ /** Derives a viewer-safe spot status without changing the supplied state. */
297
+ declare function evaluateSpotStatus(spot: Pick<SpotItem, "id" | "prerequisites">, state: Pick<UserRallyState, "records">, options?: {
298
+ readonly verifying?: boolean;
299
+ }): SpotStatus;
300
+ declare const getSpotStatus: typeof evaluateSpotStatus;
295
301
 
296
302
  declare function getOrderedSpots<TLocale extends string, TMeta extends Record<string, unknown>>(spots: ReadonlyArray<SpotItem<TLocale, TMeta>>): ReadonlyArray<SpotItem<TLocale, TMeta>>;
297
303
 
@@ -380,6 +386,10 @@ interface OfflineQueueOptions {
380
386
  removeItem?(key: string): void;
381
387
  } | null;
382
388
  readonly key?: string;
389
+ /** Rally scope used by the default durable key. */
390
+ readonly rallyId?: string;
391
+ /** User scope used by the default durable key. */
392
+ readonly userId?: string | null;
383
393
  readonly databaseName?: string;
384
394
  readonly conflictPolicy?: SyncConflictPolicy;
385
395
  readonly onSyncConflict?: SyncConflictPolicy | ((context: OfflineConflict) => SyncConflictPolicy | Promise<SyncConflictPolicy>);
@@ -389,10 +399,32 @@ interface OfflineConflict {
389
399
  readonly localState: UserRallyState;
390
400
  readonly serverState: UserRallyState;
391
401
  }
392
- type OfflineSender = (operation: OfflineOperation) => Promise<OfflineResult | OfflineConflictResult>;
402
+ type OfflineSender = (operation: OfflineOperation) => Promise<OfflineResult | OfflineConflictResult | OfflineOperationResponse>;
403
+ type OfflineOperationStatus = "ACCEPTED" | "REJECTED_PERMANENT" | "RETRYABLE_ERROR";
404
+ interface OfflineOperationError {
405
+ readonly code: string;
406
+ readonly message: string;
407
+ readonly [key: string]: unknown;
408
+ }
409
+ type OfflineOperationResponse = {
410
+ readonly status: "ACCEPTED";
411
+ readonly result?: OfflineResult | OfflineConflictResult;
412
+ readonly state?: UserRallyState;
413
+ } | {
414
+ readonly status: "REJECTED_PERMANENT";
415
+ readonly error?: ClientError | OfflineOperationError;
416
+ readonly reason?: ClientError | OfflineOperationError;
417
+ readonly state?: UserRallyState;
418
+ } | {
419
+ readonly status: "RETRYABLE_ERROR";
420
+ readonly error?: ClientError | OfflineOperationError;
421
+ readonly reason?: ClientError | OfflineOperationError;
422
+ };
393
423
  interface OfflineSyncResultEvent {
394
424
  readonly operation: OfflineOperation;
395
- readonly result: OfflineResult | OfflineConflictResult;
425
+ readonly result?: OfflineResult | OfflineConflictResult;
426
+ readonly status?: OfflineOperationStatus;
427
+ readonly error?: ClientError | OfflineOperationError;
396
428
  readonly state?: UserRallyState;
397
429
  }
398
430
  type OfflineSyncResultListener = (event: OfflineSyncResultEvent) => void | Promise<void>;
@@ -420,8 +452,14 @@ declare class OfflineQueue {
420
452
  get error(): Error | null;
421
453
  get operations(): ReadonlyArray<OfflineOperation>;
422
454
  get conflictPolicy(): SyncConflictPolicy;
455
+ get storageKey(): string;
456
+ get rallyId(): string | undefined;
457
+ get userId(): string | null;
423
458
  setSyncResultListener(listener: OfflineSyncResultListener | undefined): void;
424
459
  initialize(): Promise<void>;
460
+ /** Selects a rally/user queue scope and loads its pending operations. */
461
+ setScope(rallyId: string, userId: string | null): Promise<void>;
462
+ switchUser(newUserId: string | null): Promise<void>;
425
463
  setSender(sender: OfflineSender): void;
426
464
  enqueue(operation: OfflineOperation): Promise<void>;
427
465
  enqueueCheckIn(request: CheckInRequest): Promise<void>;
@@ -548,7 +586,7 @@ type ClientEvent = {
548
586
  readonly state: UserRallyState;
549
587
  } | {
550
588
  readonly type: "error";
551
- readonly error: ClientError;
589
+ readonly error: ClientError | OfflineOperationError;
552
590
  };
553
591
  type ClientListener = (state: UserRallyState) => void;
554
592
  type ClientEventListener = (event: ClientEvent) => void;
@@ -717,4 +755,4 @@ type SnapshotTokenVerification<T extends SnapshotTokenPayload = SnapshotTokenPay
717
755
  declare function createSignedSnapshotToken<T extends SnapshotTokenPayload>(payload: T, secretKey: SnapshotSecretKey): Promise<string>;
718
756
  declare function verifySnapshotToken<T extends SnapshotTokenPayload = SnapshotTokenPayload>(token: string, secretKey: SnapshotSecretKey, now?: number): Promise<SnapshotTokenVerification<T>>;
719
757
 
720
- export { type AdminRallyConfig, type CheckInCondition, type CheckInOptions, type CheckInRequest, type CheckInResult, type CheckInSuccess, type ClaimOptions, type ClaimRequest, type ClaimResult, type ClaimSuccess, type ClaimTicketOptions, type ClientError, type ClientEvent, type ClientEventListener, type ClientListener, type ClientOptions, type CompositeConditionFailure, type ConditionMatch, type ConditionMismatch, ConfigValidationError, type ConsumeResult, type ConsumeRewardParams, type CustomValidationContext, type CustomValidator, DEFAULT_SHEET_THEME, type DetectorError, type DetectorErrorCode, type DetectorKind, type DetectorResult, type ExternalReference, type FontFamily, type GeoDetectorOptions, type GeoVerificationContext, MemoryQueueStorage as InMemoryOfflineQueueStorage, InMemoryStorage, IndexedDBAdapter, type IndexedDBAdapterOptions, type IndexedDBOfflineQueueOptions, IndexedDBOfflineQueueStorage, LocalStorageAdapter, type LocalStorageAdapterOptions, type LocalStorageFailureMode, type LocaleDictionary, type LocalizedString, type LocalizedText, type MergeConflictOptions, type NfcDetectorOptions, type NfcVerificationContext, type OfflineConflict, type OfflineConflictResult, type OfflineOperation, OfflineQueue, type OfflineQueueOptions, type OfflineQueueStorage, type OfflineResult, type OfflineSender, type OfflineSyncResultEvent, type OfflineSyncResultListener, type ParseResult, type PasscodeCondition$1 as PasscodeCondition, type ProcessStampValue, type PublicCheckInCondition, type PublicConfigSafety, type PublicRallyConfig, type PublicReward, type PublicSpotItem, type QrDetectorOptions, type QrVerificationContext, type RallyConfig, type RallySnapshot, type RedemptionMethod, type Result, type Reward, type RewardConsumeError, type RewardState, type RewardStatus, type RewardType, type RewardUnlockCondition, type SecureTokenError, type SecureTokenErrorCode, type SecureTokenOptions, type SecureTokenPayload, type SecureTokenSecretKey, type SecureTokenVerification, type SheetTheme, type SlotShape, type SnapshotSecretKey, type SnapshotTokenError, type SnapshotTokenErrorCode, type SnapshotTokenPayload, type SnapshotTokenVerification, type SpotItem, type StampError, StampRallyClient, type StampRallyProgress, type StampRallyState, type StampRecord, type StampStorage, StorageAdapterError, type StorageAdapterErrorCode, type StorageLike, type StorageOperation, type StorageWarningHandler, type SupportedLocale, type SyncAdapter, type SyncConflictPolicy, type SyncState, THEME_PRESETS, type ThemePreset, type ThemePresetId, type UserRallyState, type ValidationError, type Validator, type VerificationContext, 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 };
758
+ export { type AdminRallyConfig, type CheckInCondition, type CheckInOptions, type CheckInRequest, type CheckInResult, type CheckInSuccess, type ClaimOptions, type ClaimRequest, type ClaimResult, type ClaimSuccess, type ClaimTicketOptions, type ClientError, type ClientEvent, type ClientEventListener, type ClientListener, type ClientOptions, type CompositeConditionFailure, type ConditionMatch, type ConditionMismatch, ConfigValidationError, type ConsumeResult, type ConsumeRewardParams, type CustomValidationContext, type CustomValidator, DEFAULT_SHEET_THEME, type DetectorError, type DetectorErrorCode, type DetectorKind, type DetectorResult, type ExternalReference, type FontFamily, type GeoDetectorOptions, type GeoVerificationContext, MemoryQueueStorage as InMemoryOfflineQueueStorage, InMemoryStorage, IndexedDBAdapter, type IndexedDBAdapterOptions, type IndexedDBOfflineQueueOptions, IndexedDBOfflineQueueStorage, LocalStorageAdapter, type LocalStorageAdapterOptions, type LocalStorageFailureMode, type LocaleDictionary, type LocalizedString, type LocalizedText, type MergeConflictOptions, type NfcDetectorOptions, type NfcVerificationContext, type OfflineConflict, type OfflineConflictResult, type OfflineOperation, type OfflineOperationError, type OfflineOperationResponse, type OfflineOperationStatus, OfflineQueue, type OfflineQueueOptions, type OfflineQueueStorage, type OfflineResult, type OfflineSender, type OfflineSyncResultEvent, type OfflineSyncResultListener, type ParseResult, type PasscodeCondition$1 as PasscodeCondition, type ProcessStampValue, type PublicCheckInCondition, type PublicConfigSafety, type PublicRallyConfig, type PublicReward, type PublicSpotItem, type QrDetectorOptions, type QrVerificationContext, type RallyConfig, type RallySnapshot, type RedemptionMethod, type Result, type Reward, type RewardConsumeError, type RewardState, type RewardStatus, type RewardType, type RewardUnlockCondition, type SecureTokenError, type SecureTokenErrorCode, type SecureTokenOptions, type SecureTokenPayload, type SecureTokenSecretKey, type SecureTokenVerification, type SheetTheme, type SlotShape, type SnapshotSecretKey, type SnapshotTokenError, type SnapshotTokenErrorCode, type SnapshotTokenPayload, type SnapshotTokenVerification, type SpotItem, type SpotStatus, type StampError, StampRallyClient, type StampRallyProgress, type StampRallyState, type StampRecord, type StampStorage, StorageAdapterError, type StorageAdapterErrorCode, type StorageLike, type StorageOperation, type StorageWarningHandler, type SupportedLocale, type SyncAdapter, type SyncConflictPolicy, type SyncState, THEME_PRESETS, type ThemePreset, type ThemePresetId, type UserRallyState, type ValidationError, type Validator, type VerificationContext, 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, verifyPasscode, verifySecureToken, verifySnapshotToken };
package/dist/index.d.ts CHANGED
@@ -147,6 +147,7 @@ interface StampRecord {
147
147
  readonly acquiredAt: string;
148
148
  readonly metadata?: Readonly<Record<string, unknown>>;
149
149
  }
150
+ type SpotStatus = "UNCLAIMED" | "CLAIMED" | "LOCKED" | "VERIFYING";
150
151
  type RewardStatus = "LOCKED" | "AVAILABLE" | "CONSUMED" | "EXPIRED";
151
152
  interface RewardState {
152
153
  readonly rewardId: string;
@@ -292,6 +293,11 @@ declare function parsePublicConfig(input: unknown): PublicRallyConfig;
292
293
  declare function calculateDistanceMeters(aLat: number, aLon: number, bLat: number, bLon: number): number;
293
294
  declare function evaluateConditionDetailed(condition: CheckInCondition, context: VerificationContext): Result<ConditionMatch, ConditionMismatch>;
294
295
  declare function evaluateCondition(condition: CheckInCondition, context: VerificationContext): boolean;
296
+ /** Derives a viewer-safe spot status without changing the supplied state. */
297
+ declare function evaluateSpotStatus(spot: Pick<SpotItem, "id" | "prerequisites">, state: Pick<UserRallyState, "records">, options?: {
298
+ readonly verifying?: boolean;
299
+ }): SpotStatus;
300
+ declare const getSpotStatus: typeof evaluateSpotStatus;
295
301
 
296
302
  declare function getOrderedSpots<TLocale extends string, TMeta extends Record<string, unknown>>(spots: ReadonlyArray<SpotItem<TLocale, TMeta>>): ReadonlyArray<SpotItem<TLocale, TMeta>>;
297
303
 
@@ -380,6 +386,10 @@ interface OfflineQueueOptions {
380
386
  removeItem?(key: string): void;
381
387
  } | null;
382
388
  readonly key?: string;
389
+ /** Rally scope used by the default durable key. */
390
+ readonly rallyId?: string;
391
+ /** User scope used by the default durable key. */
392
+ readonly userId?: string | null;
383
393
  readonly databaseName?: string;
384
394
  readonly conflictPolicy?: SyncConflictPolicy;
385
395
  readonly onSyncConflict?: SyncConflictPolicy | ((context: OfflineConflict) => SyncConflictPolicy | Promise<SyncConflictPolicy>);
@@ -389,10 +399,32 @@ interface OfflineConflict {
389
399
  readonly localState: UserRallyState;
390
400
  readonly serverState: UserRallyState;
391
401
  }
392
- type OfflineSender = (operation: OfflineOperation) => Promise<OfflineResult | OfflineConflictResult>;
402
+ type OfflineSender = (operation: OfflineOperation) => Promise<OfflineResult | OfflineConflictResult | OfflineOperationResponse>;
403
+ type OfflineOperationStatus = "ACCEPTED" | "REJECTED_PERMANENT" | "RETRYABLE_ERROR";
404
+ interface OfflineOperationError {
405
+ readonly code: string;
406
+ readonly message: string;
407
+ readonly [key: string]: unknown;
408
+ }
409
+ type OfflineOperationResponse = {
410
+ readonly status: "ACCEPTED";
411
+ readonly result?: OfflineResult | OfflineConflictResult;
412
+ readonly state?: UserRallyState;
413
+ } | {
414
+ readonly status: "REJECTED_PERMANENT";
415
+ readonly error?: ClientError | OfflineOperationError;
416
+ readonly reason?: ClientError | OfflineOperationError;
417
+ readonly state?: UserRallyState;
418
+ } | {
419
+ readonly status: "RETRYABLE_ERROR";
420
+ readonly error?: ClientError | OfflineOperationError;
421
+ readonly reason?: ClientError | OfflineOperationError;
422
+ };
393
423
  interface OfflineSyncResultEvent {
394
424
  readonly operation: OfflineOperation;
395
- readonly result: OfflineResult | OfflineConflictResult;
425
+ readonly result?: OfflineResult | OfflineConflictResult;
426
+ readonly status?: OfflineOperationStatus;
427
+ readonly error?: ClientError | OfflineOperationError;
396
428
  readonly state?: UserRallyState;
397
429
  }
398
430
  type OfflineSyncResultListener = (event: OfflineSyncResultEvent) => void | Promise<void>;
@@ -420,8 +452,14 @@ declare class OfflineQueue {
420
452
  get error(): Error | null;
421
453
  get operations(): ReadonlyArray<OfflineOperation>;
422
454
  get conflictPolicy(): SyncConflictPolicy;
455
+ get storageKey(): string;
456
+ get rallyId(): string | undefined;
457
+ get userId(): string | null;
423
458
  setSyncResultListener(listener: OfflineSyncResultListener | undefined): void;
424
459
  initialize(): Promise<void>;
460
+ /** Selects a rally/user queue scope and loads its pending operations. */
461
+ setScope(rallyId: string, userId: string | null): Promise<void>;
462
+ switchUser(newUserId: string | null): Promise<void>;
425
463
  setSender(sender: OfflineSender): void;
426
464
  enqueue(operation: OfflineOperation): Promise<void>;
427
465
  enqueueCheckIn(request: CheckInRequest): Promise<void>;
@@ -548,7 +586,7 @@ type ClientEvent = {
548
586
  readonly state: UserRallyState;
549
587
  } | {
550
588
  readonly type: "error";
551
- readonly error: ClientError;
589
+ readonly error: ClientError | OfflineOperationError;
552
590
  };
553
591
  type ClientListener = (state: UserRallyState) => void;
554
592
  type ClientEventListener = (event: ClientEvent) => void;
@@ -717,4 +755,4 @@ type SnapshotTokenVerification<T extends SnapshotTokenPayload = SnapshotTokenPay
717
755
  declare function createSignedSnapshotToken<T extends SnapshotTokenPayload>(payload: T, secretKey: SnapshotSecretKey): Promise<string>;
718
756
  declare function verifySnapshotToken<T extends SnapshotTokenPayload = SnapshotTokenPayload>(token: string, secretKey: SnapshotSecretKey, now?: number): Promise<SnapshotTokenVerification<T>>;
719
757
 
720
- export { type AdminRallyConfig, type CheckInCondition, type CheckInOptions, type CheckInRequest, type CheckInResult, type CheckInSuccess, type ClaimOptions, type ClaimRequest, type ClaimResult, type ClaimSuccess, type ClaimTicketOptions, type ClientError, type ClientEvent, type ClientEventListener, type ClientListener, type ClientOptions, type CompositeConditionFailure, type ConditionMatch, type ConditionMismatch, ConfigValidationError, type ConsumeResult, type ConsumeRewardParams, type CustomValidationContext, type CustomValidator, DEFAULT_SHEET_THEME, type DetectorError, type DetectorErrorCode, type DetectorKind, type DetectorResult, type ExternalReference, type FontFamily, type GeoDetectorOptions, type GeoVerificationContext, MemoryQueueStorage as InMemoryOfflineQueueStorage, InMemoryStorage, IndexedDBAdapter, type IndexedDBAdapterOptions, type IndexedDBOfflineQueueOptions, IndexedDBOfflineQueueStorage, LocalStorageAdapter, type LocalStorageAdapterOptions, type LocalStorageFailureMode, type LocaleDictionary, type LocalizedString, type LocalizedText, type MergeConflictOptions, type NfcDetectorOptions, type NfcVerificationContext, type OfflineConflict, type OfflineConflictResult, type OfflineOperation, OfflineQueue, type OfflineQueueOptions, type OfflineQueueStorage, type OfflineResult, type OfflineSender, type OfflineSyncResultEvent, type OfflineSyncResultListener, type ParseResult, type PasscodeCondition$1 as PasscodeCondition, type ProcessStampValue, type PublicCheckInCondition, type PublicConfigSafety, type PublicRallyConfig, type PublicReward, type PublicSpotItem, type QrDetectorOptions, type QrVerificationContext, type RallyConfig, type RallySnapshot, type RedemptionMethod, type Result, type Reward, type RewardConsumeError, type RewardState, type RewardStatus, type RewardType, type RewardUnlockCondition, type SecureTokenError, type SecureTokenErrorCode, type SecureTokenOptions, type SecureTokenPayload, type SecureTokenSecretKey, type SecureTokenVerification, type SheetTheme, type SlotShape, type SnapshotSecretKey, type SnapshotTokenError, type SnapshotTokenErrorCode, type SnapshotTokenPayload, type SnapshotTokenVerification, type SpotItem, type StampError, StampRallyClient, type StampRallyProgress, type StampRallyState, type StampRecord, type StampStorage, StorageAdapterError, type StorageAdapterErrorCode, type StorageLike, type StorageOperation, type StorageWarningHandler, type SupportedLocale, type SyncAdapter, type SyncConflictPolicy, type SyncState, THEME_PRESETS, type ThemePreset, type ThemePresetId, type UserRallyState, type ValidationError, type Validator, type VerificationContext, 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 };
758
+ export { type AdminRallyConfig, type CheckInCondition, type CheckInOptions, type CheckInRequest, type CheckInResult, type CheckInSuccess, type ClaimOptions, type ClaimRequest, type ClaimResult, type ClaimSuccess, type ClaimTicketOptions, type ClientError, type ClientEvent, type ClientEventListener, type ClientListener, type ClientOptions, type CompositeConditionFailure, type ConditionMatch, type ConditionMismatch, ConfigValidationError, type ConsumeResult, type ConsumeRewardParams, type CustomValidationContext, type CustomValidator, DEFAULT_SHEET_THEME, type DetectorError, type DetectorErrorCode, type DetectorKind, type DetectorResult, type ExternalReference, type FontFamily, type GeoDetectorOptions, type GeoVerificationContext, MemoryQueueStorage as InMemoryOfflineQueueStorage, InMemoryStorage, IndexedDBAdapter, type IndexedDBAdapterOptions, type IndexedDBOfflineQueueOptions, IndexedDBOfflineQueueStorage, LocalStorageAdapter, type LocalStorageAdapterOptions, type LocalStorageFailureMode, type LocaleDictionary, type LocalizedString, type LocalizedText, type MergeConflictOptions, type NfcDetectorOptions, type NfcVerificationContext, type OfflineConflict, type OfflineConflictResult, type OfflineOperation, type OfflineOperationError, type OfflineOperationResponse, type OfflineOperationStatus, OfflineQueue, type OfflineQueueOptions, type OfflineQueueStorage, type OfflineResult, type OfflineSender, type OfflineSyncResultEvent, type OfflineSyncResultListener, type ParseResult, type PasscodeCondition$1 as PasscodeCondition, type ProcessStampValue, type PublicCheckInCondition, type PublicConfigSafety, type PublicRallyConfig, type PublicReward, type PublicSpotItem, type QrDetectorOptions, type QrVerificationContext, type RallyConfig, type RallySnapshot, type RedemptionMethod, type Result, type Reward, type RewardConsumeError, type RewardState, type RewardStatus, type RewardType, type RewardUnlockCondition, type SecureTokenError, type SecureTokenErrorCode, type SecureTokenOptions, type SecureTokenPayload, type SecureTokenSecretKey, type SecureTokenVerification, type SheetTheme, type SlotShape, type SnapshotSecretKey, type SnapshotTokenError, type SnapshotTokenErrorCode, type SnapshotTokenPayload, type SnapshotTokenVerification, type SpotItem, type SpotStatus, type StampError, StampRallyClient, type StampRallyProgress, type StampRallyState, type StampRecord, type StampStorage, StorageAdapterError, type StorageAdapterErrorCode, type StorageLike, type StorageOperation, type StorageWarningHandler, type SupportedLocale, type SyncAdapter, type SyncConflictPolicy, type SyncState, THEME_PRESETS, type ThemePreset, type ThemePresetId, type UserRallyState, type ValidationError, type Validator, type VerificationContext, 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, verifyPasscode, verifySecureToken, verifySnapshotToken };
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) {
@@ -115,24 +123,53 @@ function mergeRewardStates(serverRewards, localRewards) {
115
123
  merged.set(localReward.rewardId, localReward);
116
124
  continue;
117
125
  }
118
- if (localReward.status === "CONSUMED" && serverReward.status !== "CONSUMED")
119
- merged.set(localReward.rewardId, localReward);
126
+ const winner = localReward.status === "CONSUMED" && serverReward.status !== "CONSUMED" ? localReward : serverReward;
127
+ merged.set(localReward.rewardId, {
128
+ ...winner,
129
+ ...serverReward.unlockedAt === void 0 && localReward.unlockedAt === void 0 ? {} : {
130
+ unlockedAt: serverReward.unlockedAt === void 0 ? localReward.unlockedAt : localReward.unlockedAt === void 0 ? serverReward.unlockedAt : latestTimestamp(serverReward.unlockedAt, localReward.unlockedAt)
131
+ },
132
+ ...serverReward.consumedAt === void 0 && localReward.consumedAt === void 0 ? {} : {
133
+ consumedAt: serverReward.consumedAt === void 0 ? localReward.consumedAt : localReward.consumedAt === void 0 ? serverReward.consumedAt : latestTimestamp(serverReward.consumedAt, localReward.consumedAt)
134
+ },
135
+ ...serverReward.redeemedCount === void 0 && localReward.redeemedCount === void 0 ? {} : {
136
+ redeemedCount: Math.max(
137
+ serverReward.redeemedCount ?? 0,
138
+ localReward.redeemedCount ?? 0
139
+ )
140
+ },
141
+ ...serverReward.userRedemptionCount === void 0 && localReward.userRedemptionCount === void 0 ? {} : {
142
+ userRedemptionCount: Math.max(
143
+ serverReward.userRedemptionCount ?? 0,
144
+ localReward.userRedemptionCount ?? 0
145
+ )
146
+ }
147
+ });
120
148
  }
121
149
  return [...merged.values()];
122
150
  }
123
- function resolveRallyStateConflict(serverState, localState, options = { policy: "merge" }) {
124
- if (options.policy === "server_wins") return serverState;
125
- const records = [...serverState.records];
126
- const knownStamps = new Set(records.map((record) => record.stampId));
127
- for (const record of localState.records) {
128
- if (!knownStamps.has(record.stampId)) {
129
- records.push(record);
130
- knownStamps.add(record.stampId);
151
+ function mergeStampRecords(serverRecords, localRecords) {
152
+ const merged = /* @__PURE__ */ new Map();
153
+ for (const record of [...serverRecords, ...localRecords]) {
154
+ const current = merged.get(record.stampId);
155
+ if (current === void 0) {
156
+ merged.set(record.stampId, record);
157
+ continue;
131
158
  }
159
+ const acquiredAt = latestTimestamp(current.acquiredAt, record.acquiredAt);
160
+ merged.set(record.stampId, {
161
+ ...current,
162
+ ...acquiredAt === current.acquiredAt ? {} : { acquiredAt },
163
+ ...current.metadata === void 0 && record.metadata !== void 0 ? { metadata: record.metadata } : {}
164
+ });
132
165
  }
166
+ return [...merged.values()];
167
+ }
168
+ function resolveRallyStateConflict(serverState, localState, options = { policy: "merge" }) {
169
+ if (options.policy === "server_wins") return serverState;
133
170
  return {
134
171
  ...serverState,
135
- records,
172
+ records: mergeStampRecords(serverState.records, localState.records),
136
173
  rewards: mergeRewardStates(serverState.rewards, localState.rewards),
137
174
  updatedAt: latestTimestamp(serverState.updatedAt, localState.updatedAt)
138
175
  };
@@ -1070,7 +1107,10 @@ var StampRallyClient = class {
1070
1107
  initialize() {
1071
1108
  if (this.#state !== null) return Promise.resolve(this.#state);
1072
1109
  if (this.#initialization === null) {
1073
- this.#initialization = this.#storage.load(this.#config.id, this.#userId).then((state) => {
1110
+ this.#initialization = (async () => {
1111
+ await this.#offlineQueue?.setScope(this.#config.id, this.#userId);
1112
+ return this.#storage.load(this.#config.id, this.#userId);
1113
+ })().then((state) => {
1074
1114
  const next = state === null ? emptyState(this.#config, this.#userId, this.#now()) : this.#reconcile(state);
1075
1115
  this.#state = next;
1076
1116
  this.#emit(next);
@@ -1088,6 +1128,7 @@ var StampRallyClient = class {
1088
1128
  this.#userId = newUserId;
1089
1129
  this.#state = null;
1090
1130
  this.#initialization = null;
1131
+ await this.#offlineQueue?.switchUser(newUserId);
1091
1132
  return this.initialize();
1092
1133
  });
1093
1134
  }
@@ -1336,7 +1377,8 @@ var StampRallyClient = class {
1336
1377
  this.#state = next;
1337
1378
  this.#emit(next);
1338
1379
  }
1339
- if ("ok" in event.result && !event.result.ok)
1380
+ if (event.error !== void 0) this.#emitEvent({ type: "error", error: event.error });
1381
+ else if (event.result !== void 0 && "ok" in event.result && !event.result.ok)
1340
1382
  this.#emitEvent({ type: "error", error: event.result.error });
1341
1383
  }
1342
1384
  #now() {
@@ -1464,9 +1506,27 @@ function defaultStorage(databaseName) {
1464
1506
  function operationId(operation) {
1465
1507
  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
1508
  }
1509
+ function requestScope(operation) {
1510
+ return {
1511
+ rallyId: operation.request.rallyId,
1512
+ userId: operation.request.userId
1513
+ };
1514
+ }
1515
+ function errorValue(value, fallbackCode) {
1516
+ if (typeof value === "object" && value !== null) {
1517
+ const candidate = value;
1518
+ if (typeof candidate.code === "string" && typeof candidate.message === "string")
1519
+ return { ...candidate, code: candidate.code, message: candidate.message };
1520
+ }
1521
+ if (value instanceof Error) return { code: fallbackCode, message: value.message };
1522
+ if (typeof value === "string") return { code: fallbackCode, message: value };
1523
+ return { code: fallbackCode, message: "Offline operation was rejected." };
1524
+ }
1467
1525
  var OfflineQueue = class {
1468
1526
  #storage;
1469
- #key;
1527
+ #configuredKey;
1528
+ #rallyId;
1529
+ #userId;
1470
1530
  #conflictPolicy;
1471
1531
  #onSyncConflict;
1472
1532
  #operations = [];
@@ -1481,7 +1541,9 @@ var OfflineQueue = class {
1481
1541
  else if (options.storageLike !== void 0 && options.storageLike !== null)
1482
1542
  this.#storage = new LocalStorageQueueStorage(options.storageLike);
1483
1543
  else this.#storage = defaultStorage(options.databaseName);
1484
- this.#key = options.key ?? "stamprally:offline-queue";
1544
+ this.#configuredKey = options.key;
1545
+ this.#rallyId = options.rallyId;
1546
+ this.#userId = options.userId ?? null;
1485
1547
  this.#conflictPolicy = options.conflictPolicy ?? "server_wins";
1486
1548
  this.#onSyncConflict = options.onSyncConflict;
1487
1549
  }
@@ -1500,23 +1562,57 @@ var OfflineQueue = class {
1500
1562
  get conflictPolicy() {
1501
1563
  return this.#conflictPolicy;
1502
1564
  }
1565
+ get storageKey() {
1566
+ return this.#storageKey();
1567
+ }
1568
+ get rallyId() {
1569
+ return this.#rallyId;
1570
+ }
1571
+ get userId() {
1572
+ return this.#userId;
1573
+ }
1503
1574
  setSyncResultListener(listener) {
1504
1575
  this.#syncResultListener = listener;
1505
1576
  }
1506
1577
  async initialize() {
1507
1578
  if (this.#loaded) return;
1508
- this.#operations = [...await this.#storage.load(this.#key)];
1579
+ this.#operations = [...await this.#storage.load(this.#storageKey())];
1509
1580
  this.#loaded = true;
1510
1581
  }
1582
+ /** Selects a rally/user queue scope and loads its pending operations. */
1583
+ async setScope(rallyId, userId) {
1584
+ if (this.#configuredKey !== void 0) {
1585
+ this.#rallyId = rallyId;
1586
+ this.#userId = userId;
1587
+ return this.initialize();
1588
+ }
1589
+ if (this.#rallyId === rallyId && this.#userId === userId && this.#loaded) return;
1590
+ this.#rallyId = rallyId;
1591
+ this.#userId = userId;
1592
+ this.#operations = [];
1593
+ this.#loaded = false;
1594
+ await this.initialize();
1595
+ }
1596
+ async switchUser(newUserId) {
1597
+ if (this.#rallyId === void 0)
1598
+ throw new Error("OfflineQueue.switchUser requires a rally scope.");
1599
+ await this.setScope(this.#rallyId, newUserId);
1600
+ }
1511
1601
  setSender(sender) {
1512
1602
  this.#sender = sender;
1513
1603
  }
1514
1604
  async enqueue(operation) {
1605
+ if (this.#configuredKey === void 0) {
1606
+ const scope = requestScope(operation);
1607
+ if (this.#rallyId === void 0) await this.setScope(scope.rallyId, scope.userId);
1608
+ if (this.#rallyId !== scope.rallyId || this.#userId !== scope.userId)
1609
+ throw new Error("Offline operation belongs to another rally or user queue.");
1610
+ }
1515
1611
  await this.initialize();
1516
1612
  const id2 = operationId(operation);
1517
1613
  if (this.#operations.some((item) => operationId(item) === id2)) return;
1518
1614
  this.#operations = [...this.#operations, operation];
1519
- await this.#storage.save(this.#key, this.#operations);
1615
+ await this.#storage.save(this.#storageKey(), this.#operations);
1520
1616
  }
1521
1617
  async enqueueCheckIn(request) {
1522
1618
  return this.enqueue({ kind: "checkIn", request });
@@ -1527,7 +1623,7 @@ var OfflineQueue = class {
1527
1623
  async clear() {
1528
1624
  await this.initialize();
1529
1625
  this.#operations = [];
1530
- await this.#storage.save(this.#key, this.#operations);
1626
+ await this.#storage.save(this.#storageKey(), this.#operations);
1531
1627
  }
1532
1628
  async sync(sender = this.#sender) {
1533
1629
  await this.initialize();
@@ -1549,20 +1645,30 @@ var OfflineQueue = class {
1549
1645
  while (this.#operations.length > 0) {
1550
1646
  const operation = this.#operations[0];
1551
1647
  if (operation === void 0) break;
1552
- let result;
1648
+ let rawResult;
1553
1649
  try {
1554
- result = await sender(operation);
1650
+ rawResult = await sender(operation);
1555
1651
  } catch (cause) {
1556
- throw cause instanceof Error ? cause : new Error(String(cause));
1652
+ throw new Error(errorValue(cause, "RETRYABLE_ERROR").message);
1653
+ }
1654
+ const response = this.#normalizeResponse(rawResult);
1655
+ if (response.status === "RETRYABLE_ERROR") {
1656
+ const error2 = errorValue(response.error ?? response.reason, "RETRYABLE_ERROR");
1657
+ await this.#syncResultListener?.({ operation, status: response.status, error: error2 });
1658
+ throw new Error(error2.message);
1557
1659
  }
1558
- const state = "conflict" in result && result.conflict === true ? await this.resolveConflict(operation, result.localState, result.serverState) : "ok" in result && result.ok ? result.value.state : void 0;
1660
+ const result = response.result;
1661
+ 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);
1662
+ const error = response.status === "REJECTED_PERMANENT" ? errorValue(response.error ?? response.reason, "REJECTED_PERMANENT") : void 0;
1663
+ this.#operations = this.#operations.slice(1);
1664
+ await this.#storage.save(this.#storageKey(), this.#operations);
1559
1665
  await this.#syncResultListener?.({
1560
1666
  operation,
1561
- result,
1667
+ ...result === void 0 ? {} : { result },
1668
+ status: response.status,
1669
+ ...error === void 0 ? {} : { error },
1562
1670
  ...state === void 0 ? {} : { state }
1563
1671
  });
1564
- this.#operations = this.#operations.slice(1);
1565
- await this.#storage.save(this.#key, this.#operations);
1566
1672
  }
1567
1673
  this.#state = "idle";
1568
1674
  } catch (cause) {
@@ -1571,6 +1677,25 @@ var OfflineQueue = class {
1571
1677
  throw this.#error;
1572
1678
  }
1573
1679
  }
1680
+ #storageKey() {
1681
+ if (this.#configuredKey !== void 0) return this.#configuredKey;
1682
+ return `stamprally:queue:${this.#rallyId ?? "unscoped"}:${this.#userId ?? "anonymous"}`;
1683
+ }
1684
+ #normalizeResponse(value) {
1685
+ if ("ok" in value) {
1686
+ if (value.ok === false) {
1687
+ if ("status" in value && value.status === "RETRYABLE_ERROR")
1688
+ return { status: "RETRYABLE_ERROR", error: errorValue(value.error, "RETRYABLE_ERROR") };
1689
+ return { status: "REJECTED_PERMANENT", result: value };
1690
+ }
1691
+ return { status: "ACCEPTED", result: value };
1692
+ }
1693
+ if ("status" in value) {
1694
+ if (value.status === "ACCEPTED") return value;
1695
+ return value;
1696
+ }
1697
+ return { status: "ACCEPTED", result: value };
1698
+ }
1574
1699
  async resolveConflict(operation, localState, serverState) {
1575
1700
  const configured = this.#onSyncConflict;
1576
1701
  const policy = (typeof configured === "function" ? await configured({ operation, localState, serverState }) : configured) ?? this.#conflictPolicy;
@@ -2463,6 +2588,6 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
2463
2588
  }
2464
2589
  }
2465
2590
 
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 };
2591
+ 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, verifyPasscode, verifySecureToken, verifySnapshotToken };
2467
2592
  //# sourceMappingURL=index.js.map
2468
2593
  //# sourceMappingURL=index.js.map