@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.cjs +204 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +204 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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(
|
|
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
|