@stamprally/core 0.13.0 → 0.14.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
@@ -96,6 +96,7 @@ type RewardUnlockCondition = {
96
96
  };
97
97
  type RewardType = "digital" | "in_person";
98
98
  type RedemptionMethod = "manual_slide" | "staff_passcode" | "view_only" | "server_claim";
99
+ type InventoryAggregationMode = "shared" | "per_reward";
99
100
  interface Reward<TLocale extends string = string> {
100
101
  readonly id: string;
101
102
  readonly title: LocalizedText<TLocale>;
@@ -121,6 +122,8 @@ interface AdminRallyConfig<TLocale extends string = string, TMeta extends Record
121
122
  readonly rewards: ReadonlyArray<Reward<TLocale>>;
122
123
  readonly staffPasscode?: string;
123
124
  readonly inventory?: Readonly<Record<string, number>>;
125
+ /** How the optional top-level inventory limit is aggregated. */
126
+ readonly inventoryMode?: InventoryAggregationMode;
124
127
  readonly serverMetadata?: Readonly<Record<string, unknown>>;
125
128
  readonly metadata?: TMeta;
126
129
  /** Explicit public metadata name. `metadata` remains supported for compatibility. */
@@ -285,6 +288,8 @@ declare class ConfigValidationError extends Error {
285
288
  readonly name = "ConfigValidationError";
286
289
  constructor(errors: ReadonlyArray<ValidationError>);
287
290
  }
291
+ /** Validates relationships that can only be checked after all entities exist. */
292
+ declare function validateRallyConfigRelations(config: AdminRallyConfig | PublicRallyConfig): ReadonlyArray<ValidationError>;
288
293
  declare function safeParseAdminConfig(input: unknown): ParseResult<AdminRallyConfig>;
289
294
  declare function parseAdminConfig(input: unknown): AdminRallyConfig;
290
295
  declare function safeParsePublicConfig(input: unknown): ParseResult<PublicRallyConfig>;
@@ -393,6 +398,8 @@ interface OfflineQueueOptions {
393
398
  readonly databaseName?: string;
394
399
  readonly conflictPolicy?: SyncConflictPolicy;
395
400
  readonly onSyncConflict?: SyncConflictPolicy | ((context: OfflineConflict) => SyncConflictPolicy | Promise<SyncConflictPolicy>);
401
+ /** Enables storage-event/BroadcastChannel synchronization when browser APIs exist. */
402
+ readonly synchronizeInstances?: boolean;
396
403
  }
397
404
  interface OfflineConflict {
398
405
  readonly operation: OfflineOperation;
@@ -457,6 +464,8 @@ declare class OfflineQueue {
457
464
  get userId(): string | null;
458
465
  setSyncResultListener(listener: OfflineSyncResultListener | undefined): void;
459
466
  initialize(): Promise<void>;
467
+ /** Releases browser listeners when the queue is no longer used. */
468
+ dispose(): void;
460
469
  /** Selects a rally/user queue scope and loads its pending operations. */
461
470
  setScope(rallyId: string, userId: string | null): Promise<void>;
462
471
  switchUser(newUserId: string | null): Promise<void>;
@@ -755,4 +764,4 @@ type SnapshotTokenVerification<T extends SnapshotTokenPayload = SnapshotTokenPay
755
764
  declare function createSignedSnapshotToken<T extends SnapshotTokenPayload>(payload: T, secretKey: SnapshotSecretKey): Promise<string>;
756
765
  declare function verifySnapshotToken<T extends SnapshotTokenPayload = SnapshotTokenPayload>(token: string, secretKey: SnapshotSecretKey, now?: number): Promise<SnapshotTokenVerification<T>>;
757
766
 
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 };
767
+ 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, type InventoryAggregationMode, 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, validateRallyConfigRelations, verifyPasscode, verifySecureToken, verifySnapshotToken };
package/dist/index.d.ts CHANGED
@@ -96,6 +96,7 @@ type RewardUnlockCondition = {
96
96
  };
97
97
  type RewardType = "digital" | "in_person";
98
98
  type RedemptionMethod = "manual_slide" | "staff_passcode" | "view_only" | "server_claim";
99
+ type InventoryAggregationMode = "shared" | "per_reward";
99
100
  interface Reward<TLocale extends string = string> {
100
101
  readonly id: string;
101
102
  readonly title: LocalizedText<TLocale>;
@@ -121,6 +122,8 @@ interface AdminRallyConfig<TLocale extends string = string, TMeta extends Record
121
122
  readonly rewards: ReadonlyArray<Reward<TLocale>>;
122
123
  readonly staffPasscode?: string;
123
124
  readonly inventory?: Readonly<Record<string, number>>;
125
+ /** How the optional top-level inventory limit is aggregated. */
126
+ readonly inventoryMode?: InventoryAggregationMode;
124
127
  readonly serverMetadata?: Readonly<Record<string, unknown>>;
125
128
  readonly metadata?: TMeta;
126
129
  /** Explicit public metadata name. `metadata` remains supported for compatibility. */
@@ -285,6 +288,8 @@ declare class ConfigValidationError extends Error {
285
288
  readonly name = "ConfigValidationError";
286
289
  constructor(errors: ReadonlyArray<ValidationError>);
287
290
  }
291
+ /** Validates relationships that can only be checked after all entities exist. */
292
+ declare function validateRallyConfigRelations(config: AdminRallyConfig | PublicRallyConfig): ReadonlyArray<ValidationError>;
288
293
  declare function safeParseAdminConfig(input: unknown): ParseResult<AdminRallyConfig>;
289
294
  declare function parseAdminConfig(input: unknown): AdminRallyConfig;
290
295
  declare function safeParsePublicConfig(input: unknown): ParseResult<PublicRallyConfig>;
@@ -393,6 +398,8 @@ interface OfflineQueueOptions {
393
398
  readonly databaseName?: string;
394
399
  readonly conflictPolicy?: SyncConflictPolicy;
395
400
  readonly onSyncConflict?: SyncConflictPolicy | ((context: OfflineConflict) => SyncConflictPolicy | Promise<SyncConflictPolicy>);
401
+ /** Enables storage-event/BroadcastChannel synchronization when browser APIs exist. */
402
+ readonly synchronizeInstances?: boolean;
396
403
  }
397
404
  interface OfflineConflict {
398
405
  readonly operation: OfflineOperation;
@@ -457,6 +464,8 @@ declare class OfflineQueue {
457
464
  get userId(): string | null;
458
465
  setSyncResultListener(listener: OfflineSyncResultListener | undefined): void;
459
466
  initialize(): Promise<void>;
467
+ /** Releases browser listeners when the queue is no longer used. */
468
+ dispose(): void;
460
469
  /** Selects a rally/user queue scope and loads its pending operations. */
461
470
  setScope(rallyId: string, userId: string | null): Promise<void>;
462
471
  switchUser(newUserId: string | null): Promise<void>;
@@ -755,4 +764,4 @@ type SnapshotTokenVerification<T extends SnapshotTokenPayload = SnapshotTokenPay
755
764
  declare function createSignedSnapshotToken<T extends SnapshotTokenPayload>(payload: T, secretKey: SnapshotSecretKey): Promise<string>;
756
765
  declare function verifySnapshotToken<T extends SnapshotTokenPayload = SnapshotTokenPayload>(token: string, secretKey: SnapshotSecretKey, now?: number): Promise<SnapshotTokenVerification<T>>;
757
766
 
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 };
767
+ 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, type InventoryAggregationMode, 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, validateRallyConfigRelations, verifyPasscode, verifySecureToken, verifySnapshotToken };
package/dist/index.js CHANGED
@@ -59,7 +59,9 @@ function calculateProgress(state, config) {
59
59
  const acquired = new Set(
60
60
  state.records.map((record) => record.stampId).filter((id2) => ids.has(id2))
61
61
  );
62
- const remaining = config.spots.filter((spot2) => !acquired.has(spot2.id));
62
+ const remaining = config.spots.filter(
63
+ (spot2) => !acquired.has(spot2.id) && (spot2.prerequisites === void 0 || spot2.prerequisites.every((id2) => acquired.has(id2)))
64
+ );
63
65
  return {
64
66
  acquired: acquired.size,
65
67
  total: config.spots.length,
@@ -1522,6 +1524,11 @@ function errorValue(value, fallbackCode) {
1522
1524
  if (typeof value === "string") return { code: fallbackCode, message: value };
1523
1525
  return { code: fallbackCode, message: "Offline operation was rejected." };
1524
1526
  }
1527
+ var syncLocks = /* @__PURE__ */ new Map();
1528
+ var SYNC_LOCK_TTL_MS = 3e4;
1529
+ function randomId() {
1530
+ return globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
1531
+ }
1525
1532
  var OfflineQueue = class {
1526
1533
  #storage;
1527
1534
  #configuredKey;
@@ -1536,6 +1543,11 @@ var OfflineQueue = class {
1536
1543
  #sender;
1537
1544
  #syncPromise = null;
1538
1545
  #syncResultListener;
1546
+ #synchronizeInstances;
1547
+ #instanceId = randomId();
1548
+ #lockStorage;
1549
+ #storageListener;
1550
+ #channel = null;
1539
1551
  constructor(options = {}) {
1540
1552
  if (options.storage !== void 0) this.#storage = options.storage;
1541
1553
  else if (options.storageLike !== void 0 && options.storageLike !== null)
@@ -1546,6 +1558,9 @@ var OfflineQueue = class {
1546
1558
  this.#userId = options.userId ?? null;
1547
1559
  this.#conflictPolicy = options.conflictPolicy ?? "server_wins";
1548
1560
  this.#onSyncConflict = options.onSyncConflict;
1561
+ this.#synchronizeInstances = options.synchronizeInstances ?? true;
1562
+ this.#lockStorage = options.storageLike ?? globalThis.localStorage ?? null;
1563
+ if (this.#synchronizeInstances) this.#subscribeToExternalChanges();
1549
1564
  }
1550
1565
  get syncState() {
1551
1566
  return this.#state;
@@ -1579,6 +1594,16 @@ var OfflineQueue = class {
1579
1594
  this.#operations = [...await this.#storage.load(this.#storageKey())];
1580
1595
  this.#loaded = true;
1581
1596
  }
1597
+ /** Releases browser listeners when the queue is no longer used. */
1598
+ dispose() {
1599
+ const windowLike = globalThis.window;
1600
+ if (windowLike !== void 0 && this.#storageListener !== void 0)
1601
+ windowLike.removeEventListener("storage", this.#storageListener);
1602
+ this.#storageListener = void 0;
1603
+ this.#channel?.close();
1604
+ this.#channel = null;
1605
+ this.#releaseSyncLock();
1606
+ }
1582
1607
  /** Selects a rally/user queue scope and loads its pending operations. */
1583
1608
  async setScope(rallyId, userId) {
1584
1609
  if (this.#configuredKey !== void 0) {
@@ -1613,6 +1638,7 @@ var OfflineQueue = class {
1613
1638
  if (this.#operations.some((item) => operationId(item) === id2)) return;
1614
1639
  this.#operations = [...this.#operations, operation];
1615
1640
  await this.#storage.save(this.#storageKey(), this.#operations);
1641
+ this.#announceChange();
1616
1642
  }
1617
1643
  async enqueueCheckIn(request) {
1618
1644
  return this.enqueue({ kind: "checkIn", request });
@@ -1624,6 +1650,7 @@ var OfflineQueue = class {
1624
1650
  await this.initialize();
1625
1651
  this.#operations = [];
1626
1652
  await this.#storage.save(this.#storageKey(), this.#operations);
1653
+ this.#announceChange();
1627
1654
  }
1628
1655
  async sync(sender = this.#sender) {
1629
1656
  await this.initialize();
@@ -1641,6 +1668,11 @@ var OfflineQueue = class {
1641
1668
  async #run(sender) {
1642
1669
  this.#state = "syncing";
1643
1670
  this.#error = null;
1671
+ if (!this.#acquireSyncLock()) {
1672
+ await this.#reloadFromStorage();
1673
+ this.#state = "idle";
1674
+ return;
1675
+ }
1644
1676
  try {
1645
1677
  while (this.#operations.length > 0) {
1646
1678
  const operation = this.#operations[0];
@@ -1662,6 +1694,7 @@ var OfflineQueue = class {
1662
1694
  const error = response.status === "REJECTED_PERMANENT" ? errorValue(response.error ?? response.reason, "REJECTED_PERMANENT") : void 0;
1663
1695
  this.#operations = this.#operations.slice(1);
1664
1696
  await this.#storage.save(this.#storageKey(), this.#operations);
1697
+ this.#announceChange();
1665
1698
  await this.#syncResultListener?.({
1666
1699
  operation,
1667
1700
  ...result === void 0 ? {} : { result },
@@ -1675,6 +1708,80 @@ var OfflineQueue = class {
1675
1708
  this.#state = "error";
1676
1709
  this.#error = cause instanceof Error ? cause : new Error(String(cause));
1677
1710
  throw this.#error;
1711
+ } finally {
1712
+ this.#releaseSyncLock();
1713
+ }
1714
+ }
1715
+ #subscribeToExternalChanges() {
1716
+ const windowLike = globalThis.window;
1717
+ if (windowLike !== void 0) {
1718
+ this.#storageListener = (event) => {
1719
+ if (event.key === this.#storageKey()) void this.#reloadFromStorage();
1720
+ };
1721
+ windowLike.addEventListener("storage", this.#storageListener);
1722
+ }
1723
+ const Channel = globalThis.BroadcastChannel;
1724
+ if (Channel !== void 0) {
1725
+ try {
1726
+ this.#channel = new Channel("stamprally:queue-sync");
1727
+ this.#channel.addEventListener("message", (event) => {
1728
+ if (typeof event.data === "object" && event.data !== null && "key" in event.data && event.data.key === this.#storageKey())
1729
+ void this.#reloadFromStorage();
1730
+ });
1731
+ } catch {
1732
+ this.#channel = null;
1733
+ }
1734
+ }
1735
+ }
1736
+ #announceChange() {
1737
+ this.#channel?.postMessage({ key: this.#storageKey(), owner: this.#instanceId });
1738
+ }
1739
+ async #reloadFromStorage() {
1740
+ if (this.#state === "syncing") return;
1741
+ try {
1742
+ this.#operations = [...await this.#storage.load(this.#storageKey())];
1743
+ this.#loaded = true;
1744
+ } catch {
1745
+ }
1746
+ }
1747
+ #lockKey() {
1748
+ return `${this.#storageKey()}:sync-lock`;
1749
+ }
1750
+ #acquireSyncLock() {
1751
+ const key = this.#lockKey();
1752
+ const now = Date.now();
1753
+ const local = syncLocks.get(key);
1754
+ if (local !== void 0 && local.expiresAt > now && local.owner !== this.#instanceId)
1755
+ return false;
1756
+ if (this.#lockStorage !== null) {
1757
+ try {
1758
+ const existing = this.#lockStorage.getItem(key);
1759
+ if (existing !== null) {
1760
+ const parsed = JSON.parse(existing);
1761
+ if (typeof parsed === "object" && parsed !== null && typeof parsed.expiresAt === "number" && parsed.expiresAt > now && parsed.owner !== this.#instanceId)
1762
+ return false;
1763
+ }
1764
+ this.#lockStorage.setItem(
1765
+ key,
1766
+ JSON.stringify({ owner: this.#instanceId, expiresAt: now + SYNC_LOCK_TTL_MS })
1767
+ );
1768
+ } catch {
1769
+ }
1770
+ }
1771
+ syncLocks.set(key, { owner: this.#instanceId, expiresAt: now + SYNC_LOCK_TTL_MS });
1772
+ return true;
1773
+ }
1774
+ #releaseSyncLock() {
1775
+ const key = this.#lockKey();
1776
+ const current = syncLocks.get(key);
1777
+ if (current?.owner === this.#instanceId) syncLocks.delete(key);
1778
+ if (this.#lockStorage !== null) {
1779
+ try {
1780
+ const value = this.#lockStorage.getItem(key);
1781
+ if (value !== null && JSON.parse(value).owner === this.#instanceId)
1782
+ this.#lockStorage.removeItem?.(key);
1783
+ } catch {
1784
+ }
1678
1785
  }
1679
1786
  }
1680
1787
  #storageKey() {
@@ -2437,21 +2544,112 @@ function validate(value, isPublic) {
2437
2544
  optionalString(value, "staffPasscode", "$", errors);
2438
2545
  if (hasOwn(value, "inventory") && value.inventory !== void 0 && !isRecord3(value.inventory))
2439
2546
  add(errors, "$.inventory", "Expected an object.", "invalid_type");
2547
+ if (hasOwn(value, "inventoryMode") && value.inventoryMode !== void 0 && value.inventoryMode !== "shared" && value.inventoryMode !== "per_reward")
2548
+ add(errors, "$.inventoryMode", "Expected shared or per_reward.", "invalid_enum");
2440
2549
  if (hasOwn(value, "serverMetadata") && value.serverMetadata !== void 0 && !isRecord3(value.serverMetadata))
2441
2550
  add(errors, "$.serverMetadata", "Expected an object.", "invalid_type");
2442
2551
  if (hasOwn(value, "publicMetadata") && value.publicMetadata !== void 0 && !isRecord3(value.publicMetadata))
2443
2552
  add(errors, "$.publicMetadata", "Expected an object.", "invalid_type");
2444
2553
  optionalString(value, "serverEndpoint", "$", errors);
2445
2554
  } else {
2446
- for (const key of ["staffPasscode", "serverMetadata", "inventory"])
2555
+ for (const key of ["staffPasscode", "serverMetadata", "inventory", "inventoryMode"])
2447
2556
  if (hasOwn(value, key))
2448
2557
  add(errors, `$.${key}`, "Private field is not allowed.", "private_field");
2449
2558
  optionalString(value, "serverEndpoint", "$", errors);
2450
2559
  }
2451
2560
  return errors;
2452
2561
  }
2562
+ function validateRallyConfigRelations(config) {
2563
+ const errors = [];
2564
+ const spotIds = /* @__PURE__ */ new Set();
2565
+ const rewardIds = /* @__PURE__ */ new Set();
2566
+ const orderIndexes = /* @__PURE__ */ new Map();
2567
+ config.spots.forEach((spot2, index) => {
2568
+ if (spotIds.has(spot2.id))
2569
+ add(errors, `spots[${index}].id`, "Spot ID must be unique.", "duplicate_spot_id");
2570
+ spotIds.add(spot2.id);
2571
+ const previousIndex = orderIndexes.get(spot2.orderIndex);
2572
+ if (previousIndex !== void 0)
2573
+ add(
2574
+ errors,
2575
+ `spots[${index}].orderIndex`,
2576
+ `orderIndex duplicates spots[${previousIndex}].`,
2577
+ "duplicate_order_index"
2578
+ );
2579
+ else orderIndexes.set(spot2.orderIndex, index);
2580
+ if (spot2.orderIndex < 0)
2581
+ add(
2582
+ errors,
2583
+ `spots[${index}].orderIndex`,
2584
+ "orderIndex must not be negative.",
2585
+ "negative_order_index"
2586
+ );
2587
+ spot2.prerequisites?.forEach((prerequisite, prerequisiteIndex) => {
2588
+ if (!spotIds.has(prerequisite) && !config.spots.some((candidate) => candidate.id === prerequisite))
2589
+ add(
2590
+ errors,
2591
+ `spots[${index}].prerequisites[${prerequisiteIndex}]`,
2592
+ "Prerequisite spot does not exist.",
2593
+ "missing_prerequisite"
2594
+ );
2595
+ });
2596
+ });
2597
+ config.rewards.forEach((reward2, index) => {
2598
+ if (rewardIds.has(reward2.id))
2599
+ add(errors, `rewards[${index}].id`, "Reward ID must be unique.", "duplicate_reward_id");
2600
+ rewardIds.add(reward2.id);
2601
+ const visit = (condition2, path) => {
2602
+ if (condition2.type === "stamps")
2603
+ condition2.stampIds.forEach((stampId, stampIndex) => {
2604
+ if (!spotIds.has(stampId))
2605
+ add(
2606
+ errors,
2607
+ `${path}.stampIds[${stampIndex}]`,
2608
+ "Referenced spot does not exist.",
2609
+ "missing_reward_spot"
2610
+ );
2611
+ });
2612
+ else if (condition2.type === "all" || condition2.type === "any")
2613
+ condition2.conditions.forEach((nested, nestedIndex) => {
2614
+ visit(nested, `${path}.conditions[${nestedIndex}]`);
2615
+ });
2616
+ };
2617
+ reward2.conditions?.forEach((condition2, conditionIndex) => {
2618
+ visit(condition2, `rewards[${index}].conditions[${conditionIndex}]`);
2619
+ });
2620
+ });
2621
+ const visiting = /* @__PURE__ */ new Set();
2622
+ const visited = /* @__PURE__ */ new Set();
2623
+ const cycleNodes = /* @__PURE__ */ new Set();
2624
+ const visitSpot = (spotId) => {
2625
+ if (visiting.has(spotId)) {
2626
+ cycleNodes.add(spotId);
2627
+ return;
2628
+ }
2629
+ if (visited.has(spotId)) return;
2630
+ visiting.add(spotId);
2631
+ const spot2 = config.spots.find((candidate) => candidate.id === spotId);
2632
+ spot2?.prerequisites?.forEach(visitSpot);
2633
+ visiting.delete(spotId);
2634
+ visited.add(spotId);
2635
+ };
2636
+ config.spots.forEach((spot2) => {
2637
+ visitSpot(spot2.id);
2638
+ });
2639
+ cycleNodes.forEach((spotId) => {
2640
+ const index = config.spots.findIndex((spot2) => spot2.id === spotId);
2641
+ add(
2642
+ errors,
2643
+ `spots[${index}].prerequisites`,
2644
+ "Prerequisites must form a DAG.",
2645
+ "cyclic_prerequisites"
2646
+ );
2647
+ });
2648
+ return errors;
2649
+ }
2453
2650
  function safeParseAdminConfig(input) {
2454
- const errors = validate(input, false);
2651
+ const errors = [...validate(input, false)];
2652
+ if (errors.length === 0) errors.push(...validateRallyConfigRelations(input));
2455
2653
  return errors.length === 0 ? { success: true, data: input } : { success: false, errors };
2456
2654
  }
2457
2655
  function parseAdminConfig(input) {
@@ -2460,7 +2658,8 @@ function parseAdminConfig(input) {
2460
2658
  return result.data;
2461
2659
  }
2462
2660
  function safeParsePublicConfig(input) {
2463
- const errors = validate(input, true);
2661
+ const errors = [...validate(input, true)];
2662
+ if (errors.length === 0) errors.push(...validateRallyConfigRelations(input));
2464
2663
  return errors.length === 0 ? { success: true, data: input } : { success: false, errors };
2465
2664
  }
2466
2665
  function parsePublicConfig(input) {
@@ -2588,6 +2787,6 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
2588
2787
  }
2589
2788
  }
2590
2789
 
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 };
2790
+ 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, validateRallyConfigRelations, verifyPasscode, verifySecureToken, verifySnapshotToken };
2592
2791
  //# sourceMappingURL=index.js.map
2593
2792
  //# sourceMappingURL=index.js.map