achievements-engine 1.0.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.
@@ -0,0 +1,635 @@
1
+ /**
2
+ * Lightweight, type-safe event emitter for the achievements engine
3
+ * Zero dependencies, memory-leak safe implementation
4
+ */
5
+ type EventHandler$1<T = any> = (data: T) => void;
6
+ type UnsubscribeFn = () => void;
7
+ declare class EventEmitter {
8
+ private listeners;
9
+ private onceListeners;
10
+ constructor();
11
+ /**
12
+ * Subscribe to an event
13
+ * @param event - Event name
14
+ * @param handler - Event handler function
15
+ * @returns Unsubscribe function
16
+ */
17
+ on<T = any>(event: string, handler: EventHandler$1<T>): UnsubscribeFn;
18
+ /**
19
+ * Subscribe to an event once (auto-unsubscribes after first emission)
20
+ * @param event - Event name
21
+ * @param handler - Event handler function
22
+ * @returns Unsubscribe function
23
+ */
24
+ once<T = any>(event: string, handler: EventHandler$1<T>): UnsubscribeFn;
25
+ /**
26
+ * Unsubscribe from an event
27
+ * @param event - Event name
28
+ * @param handler - Event handler function to remove
29
+ */
30
+ off<T = any>(event: string, handler: EventHandler$1<T>): void;
31
+ /**
32
+ * Emit an event to all subscribers
33
+ * @param event - Event name
34
+ * @param data - Event payload
35
+ */
36
+ emit<T = any>(event: string, data?: T): void;
37
+ /**
38
+ * Remove all listeners for a specific event, or all events if no event specified
39
+ * @param event - Optional event name. If not provided, removes all listeners.
40
+ */
41
+ removeAllListeners(event?: string): void;
42
+ /**
43
+ * Get the number of listeners for an event
44
+ * @param event - Event name
45
+ * @returns Number of listeners
46
+ */
47
+ listenerCount(event: string): number;
48
+ /**
49
+ * Get all event names that have listeners
50
+ * @returns Array of event names
51
+ */
52
+ eventNames(): string[];
53
+ }
54
+
55
+ /**
56
+ * Type definitions for the achievements engine
57
+ * Framework-agnostic achievement system types
58
+ */
59
+
60
+ type AchievementMetricValue = number | string | boolean | Date | null | undefined;
61
+ type AchievementMetricArrayValue = AchievementMetricValue | AchievementMetricValue[];
62
+ interface AchievementMetrics {
63
+ [key: string]: AchievementMetricValue[];
64
+ }
65
+ interface AchievementDetails {
66
+ achievementId: string;
67
+ achievementTitle: string;
68
+ achievementDescription: string;
69
+ achievementIconKey?: string;
70
+ }
71
+ interface AchievementWithStatus extends AchievementDetails {
72
+ isUnlocked: boolean;
73
+ }
74
+ interface AchievementCondition {
75
+ isConditionMet: (value: AchievementMetricArrayValue, state: AchievementState) => boolean;
76
+ achievementDetails: AchievementDetails | AchievementWithStatus;
77
+ }
78
+ interface AchievementConfiguration {
79
+ [key: string]: AchievementCondition[];
80
+ }
81
+ interface SimpleAchievementDetails {
82
+ title: string;
83
+ description?: string;
84
+ icon?: string;
85
+ }
86
+ interface CustomAchievementDetails extends SimpleAchievementDetails {
87
+ condition: (metrics: Record<string, any>) => boolean;
88
+ }
89
+ interface SimpleAchievementConfig {
90
+ [metric: string]: {
91
+ [threshold: string]: SimpleAchievementDetails | CustomAchievementDetails;
92
+ };
93
+ }
94
+ type AchievementConfigurationType = AchievementConfiguration | SimpleAchievementConfig;
95
+ interface InitialAchievementMetrics {
96
+ [key: string]: AchievementMetricValue;
97
+ }
98
+ interface AchievementState {
99
+ metrics: AchievementMetrics;
100
+ unlockedAchievements: string[];
101
+ }
102
+ interface AchievementStorage {
103
+ getMetrics(): AchievementMetrics;
104
+ setMetrics(metrics: AchievementMetrics): void;
105
+ getUnlockedAchievements(): string[];
106
+ setUnlockedAchievements(achievements: string[]): void;
107
+ clear(): void;
108
+ }
109
+ interface AsyncAchievementStorage {
110
+ getMetrics(): Promise<AchievementMetrics>;
111
+ setMetrics(metrics: AchievementMetrics): Promise<void>;
112
+ getUnlockedAchievements(): Promise<string[]>;
113
+ setUnlockedAchievements(achievements: string[]): Promise<void>;
114
+ clear(): Promise<void>;
115
+ }
116
+ type AnyAchievementStorage = AchievementStorage | AsyncAchievementStorage;
117
+ declare function isAsyncStorage(storage: AnyAchievementStorage): storage is AsyncAchievementStorage;
118
+ declare enum StorageType {
119
+ Local = "local",// Synchronous localStorage
120
+ Memory = "memory",// Synchronous in-memory storage
121
+ IndexedDB = "indexeddb",// Asynchronous IndexedDB storage
122
+ RestAPI = "restapi"
123
+ }
124
+ /**
125
+ * Event types emitted by the engine
126
+ */
127
+ type EngineEvent = 'achievement:unlocked' | 'metric:updated' | 'state:changed' | 'error';
128
+ /**
129
+ * Event payload when an achievement is unlocked
130
+ */
131
+ interface AchievementUnlockedEvent {
132
+ achievementId: string;
133
+ achievementTitle: string;
134
+ achievementDescription: string;
135
+ achievementIconKey?: string;
136
+ timestamp: number;
137
+ }
138
+ /**
139
+ * Event payload when a metric is updated
140
+ */
141
+ interface MetricUpdatedEvent {
142
+ metric: string;
143
+ oldValue: any;
144
+ newValue: any;
145
+ timestamp: number;
146
+ }
147
+ /**
148
+ * Event payload when overall state changes
149
+ */
150
+ interface StateChangedEvent {
151
+ metrics: AchievementMetrics;
152
+ unlocked: string[];
153
+ timestamp: number;
154
+ }
155
+ /**
156
+ * Event payload when an error occurs
157
+ */
158
+ interface ErrorEvent {
159
+ error: Error;
160
+ context?: string;
161
+ timestamp: number;
162
+ }
163
+ /**
164
+ * Event handler type
165
+ */
166
+ type EventHandler<T = any> = (data: T) => void;
167
+ /**
168
+ * Metric updater function for custom event-to-metric mapping
169
+ */
170
+ type MetricUpdater = (eventData: any, currentMetrics: Record<string, any>) => Record<string, any>;
171
+ /**
172
+ * Event mapping configuration
173
+ * Maps event names to either:
174
+ * - String (metric name) for direct 1:1 mapping
175
+ * - MetricUpdater function for custom transformation
176
+ */
177
+ interface EventMapping {
178
+ [eventName: string]: string | MetricUpdater;
179
+ }
180
+ /**
181
+ * REST API storage configuration
182
+ */
183
+ interface RestApiStorageConfig$1 {
184
+ baseUrl: string;
185
+ userId: string;
186
+ headers?: Record<string, string>;
187
+ timeout?: number;
188
+ }
189
+ /**
190
+ * Configuration for the Achievement Engine
191
+ */
192
+ interface EngineConfig {
193
+ /**
194
+ * Achievement configuration (Simple or Complex API format)
195
+ */
196
+ achievements: AchievementConfigurationType;
197
+ /**
198
+ * Storage implementation or storage type
199
+ * Defaults to memory storage
200
+ */
201
+ storage?: AchievementStorage | AsyncAchievementStorage | StorageType;
202
+ /**
203
+ * Optional event-to-metric mapping
204
+ * Enables event-based tracking with emit()
205
+ */
206
+ eventMapping?: EventMapping;
207
+ /**
208
+ * Error handler for async operations and achievement errors
209
+ */
210
+ onError?: (error: Error) => void;
211
+ /**
212
+ * REST API configuration (required if using StorageType.RestAPI)
213
+ */
214
+ restApiConfig?: RestApiStorageConfig$1;
215
+ }
216
+ interface ImportOptions$1 {
217
+ merge?: boolean;
218
+ overwrite?: boolean;
219
+ validateConfig?: boolean;
220
+ expectedConfigHash?: string;
221
+ }
222
+ interface ImportResult$1 {
223
+ success: boolean;
224
+ errors?: string[];
225
+ warnings?: string[];
226
+ mergedMetrics?: AchievementMetrics;
227
+ mergedUnlocked?: string[];
228
+ }
229
+ /**
230
+ * Public API surface of the AchievementEngine
231
+ * This type represents the stable, supported API for external consumers
232
+ * Derived from the AchievementEngine class to prevent duplication
233
+ */
234
+ type AchievementEngineApi = Pick<AchievementEngine, 'emit' | 'update' | 'on' | 'once' | 'off' | 'getMetrics' | 'getUnlocked' | 'getAllAchievements' | 'reset' | 'export' | 'import'>;
235
+
236
+ /**
237
+ * AchievementEngine - Framework-agnostic achievement system
238
+ * Event-based core with support for multiple storage backends
239
+ */
240
+
241
+ declare class AchievementEngine extends EventEmitter {
242
+ private config;
243
+ private achievements;
244
+ private storage;
245
+ private metrics;
246
+ private unlockedAchievements;
247
+ private configHash;
248
+ constructor(config: EngineConfig);
249
+ /**
250
+ * Initialize storage based on configuration
251
+ */
252
+ private initializeStorage;
253
+ /**
254
+ * Load state from storage
255
+ */
256
+ private loadFromStorage;
257
+ /**
258
+ * Save state to storage
259
+ */
260
+ private saveToStorage;
261
+ /**
262
+ * Handle errors with optional callback
263
+ */
264
+ private handleError;
265
+ /**
266
+ * Emit a custom event and optionally update metrics based on event mapping
267
+ * @param eventName - Name of the event
268
+ * @param data - Event data
269
+ */
270
+ emit<T = any>(eventName: string, data?: T): void;
271
+ /**
272
+ * Update metrics and evaluate achievements
273
+ * @param newMetrics - Metrics to update
274
+ */
275
+ update(newMetrics: Record<string, any>): void;
276
+ /**
277
+ * Evaluate all achievements and unlock any newly met conditions
278
+ * This is the core evaluation logic extracted from AchievementProvider
279
+ */
280
+ private evaluateAchievements;
281
+ /**
282
+ * Get metrics in array format (for backward compatibility with storage)
283
+ */
284
+ private getMetricsAsArray;
285
+ /**
286
+ * Get current metrics (readonly to prevent external modification)
287
+ */
288
+ getMetrics(): Readonly<Record<string, any>>;
289
+ /**
290
+ * Get unlocked achievement IDs (readonly)
291
+ */
292
+ getUnlocked(): readonly string[];
293
+ /**
294
+ * Get all achievements with their unlock status
295
+ */
296
+ getAllAchievements(): AchievementWithStatus[];
297
+ /**
298
+ * Reset all achievement data
299
+ */
300
+ reset(): void;
301
+ /**
302
+ * Clean up resources and event listeners
303
+ */
304
+ destroy(): void;
305
+ /**
306
+ * Export achievement data as JSON string
307
+ */
308
+ export(): string;
309
+ /**
310
+ * Import achievement data from JSON string
311
+ * @param jsonString - Exported achievement data
312
+ * @param options - Import options
313
+ */
314
+ import(jsonString: string, options?: ImportOptions$1): ImportResult$1;
315
+ /**
316
+ * Subscribe to engine events
317
+ * @param event - Event name
318
+ * @param handler - Event handler
319
+ */
320
+ on(event: EngineEvent, handler: (data: any) => void): UnsubscribeFn;
321
+ /**
322
+ * Subscribe to an event once
323
+ * @param event - Event name
324
+ * @param handler - Event handler
325
+ */
326
+ once(event: EngineEvent, handler: (data: any) => void): UnsubscribeFn;
327
+ /**
328
+ * Unsubscribe from an event
329
+ * @param event - Event name
330
+ * @param handler - Event handler
331
+ */
332
+ off(event: EngineEvent, handler: (data: any) => void): void;
333
+ }
334
+
335
+ declare class LocalStorage implements AchievementStorage {
336
+ private storageKey;
337
+ constructor(storageKey: string);
338
+ private serializeValue;
339
+ private deserializeValue;
340
+ private serializeMetrics;
341
+ private deserializeMetrics;
342
+ private getStorageData;
343
+ private setStorageData;
344
+ getMetrics(): AchievementMetrics;
345
+ setMetrics(metrics: AchievementMetrics): void;
346
+ getUnlockedAchievements(): string[];
347
+ setUnlockedAchievements(achievements: string[]): void;
348
+ clear(): void;
349
+ }
350
+
351
+ declare class MemoryStorage implements AchievementStorage {
352
+ private metrics;
353
+ private unlockedAchievements;
354
+ constructor();
355
+ getMetrics(): AchievementMetrics;
356
+ setMetrics(metrics: AchievementMetrics): void;
357
+ getUnlockedAchievements(): string[];
358
+ setUnlockedAchievements(achievements: string[]): void;
359
+ clear(): void;
360
+ }
361
+
362
+ declare class IndexedDBStorage implements AsyncAchievementStorage {
363
+ private dbName;
364
+ private storeName;
365
+ private db;
366
+ private initPromise;
367
+ constructor(dbName?: string);
368
+ /**
369
+ * Initialize IndexedDB database and object store
370
+ */
371
+ private initDB;
372
+ /**
373
+ * Generic get operation from IndexedDB
374
+ */
375
+ private get;
376
+ /**
377
+ * Generic set operation to IndexedDB
378
+ */
379
+ private set;
380
+ /**
381
+ * Delete operation from IndexedDB
382
+ */
383
+ private delete;
384
+ getMetrics(): Promise<AchievementMetrics>;
385
+ setMetrics(metrics: AchievementMetrics): Promise<void>;
386
+ getUnlockedAchievements(): Promise<string[]>;
387
+ setUnlockedAchievements(achievements: string[]): Promise<void>;
388
+ clear(): Promise<void>;
389
+ /**
390
+ * Close the database connection
391
+ */
392
+ close(): void;
393
+ }
394
+
395
+ interface RestApiStorageConfig {
396
+ baseUrl: string;
397
+ userId: string;
398
+ headers?: Record<string, string>;
399
+ timeout?: number;
400
+ }
401
+ declare class RestApiStorage implements AsyncAchievementStorage {
402
+ private config;
403
+ constructor(config: RestApiStorageConfig);
404
+ /**
405
+ * Generic fetch wrapper with timeout and error handling
406
+ */
407
+ private fetchWithTimeout;
408
+ getMetrics(): Promise<AchievementMetrics>;
409
+ setMetrics(metrics: AchievementMetrics): Promise<void>;
410
+ getUnlockedAchievements(): Promise<string[]>;
411
+ setUnlockedAchievements(achievements: string[]): Promise<void>;
412
+ clear(): Promise<void>;
413
+ }
414
+
415
+ /**
416
+ * Base error class for all achievement-related errors
417
+ */
418
+ declare class AchievementError extends Error {
419
+ code: string;
420
+ recoverable: boolean;
421
+ remedy?: string | undefined;
422
+ constructor(message: string, code: string, recoverable: boolean, remedy?: string | undefined);
423
+ }
424
+ /**
425
+ * Error thrown when browser storage quota is exceeded
426
+ */
427
+ declare class StorageQuotaError extends AchievementError {
428
+ bytesNeeded: number;
429
+ constructor(bytesNeeded: number);
430
+ }
431
+ /**
432
+ * Error thrown when imported data fails validation
433
+ */
434
+ declare class ImportValidationError extends AchievementError {
435
+ validationErrors: string[];
436
+ constructor(validationErrors: string[]);
437
+ }
438
+ /**
439
+ * Error thrown when storage operations fail
440
+ */
441
+ declare class StorageError extends AchievementError {
442
+ originalError?: Error | undefined;
443
+ constructor(message: string, originalError?: Error | undefined);
444
+ }
445
+ /**
446
+ * Error thrown when configuration is invalid
447
+ */
448
+ declare class ConfigurationError extends AchievementError {
449
+ constructor(message: string);
450
+ }
451
+ /**
452
+ * Error thrown when network sync operations fail
453
+ */
454
+ declare class SyncError extends AchievementError {
455
+ readonly statusCode?: number;
456
+ readonly timeout?: number;
457
+ constructor(message: string, details?: {
458
+ statusCode?: number;
459
+ timeout?: number;
460
+ });
461
+ }
462
+ /**
463
+ * Type guard to check if an error is an AchievementError
464
+ */
465
+ declare function isAchievementError(error: unknown): error is AchievementError;
466
+ /**
467
+ * Type guard to check if an error is recoverable
468
+ */
469
+ declare function isRecoverableError(error: unknown): boolean;
470
+
471
+ declare class AsyncStorageAdapter implements AchievementStorage {
472
+ private asyncStorage;
473
+ private cache;
474
+ private pendingWrites;
475
+ private onError?;
476
+ constructor(asyncStorage: AsyncAchievementStorage, options?: {
477
+ onError?: (error: AchievementError) => void;
478
+ });
479
+ /**
480
+ * Initialize cache by loading from async storage
481
+ * This happens in the background during construction
482
+ */
483
+ private initializeCache;
484
+ /**
485
+ * Wait for cache to be loaded (used internally)
486
+ * Returns immediately if already loaded, otherwise waits
487
+ */
488
+ private ensureCacheLoaded;
489
+ /**
490
+ * SYNC READ: Returns cached metrics immediately
491
+ * Cache is loaded eagerly during construction
492
+ */
493
+ getMetrics(): AchievementMetrics;
494
+ /**
495
+ * SYNC WRITE: Updates cache immediately, writes to storage in background
496
+ * Uses optimistic updates - assumes write will succeed
497
+ */
498
+ setMetrics(metrics: AchievementMetrics): void;
499
+ /**
500
+ * SYNC READ: Returns cached unlocked achievements immediately
501
+ */
502
+ getUnlockedAchievements(): string[];
503
+ /**
504
+ * SYNC WRITE: Updates cache immediately, writes to storage in background
505
+ */
506
+ setUnlockedAchievements(achievements: string[]): void;
507
+ /**
508
+ * SYNC CLEAR: Clears cache immediately, clears storage in background
509
+ */
510
+ clear(): void;
511
+ /**
512
+ * Wait for all pending writes to complete (useful for testing/cleanup)
513
+ * NOT part of AchievementStorage interface - utility method
514
+ */
515
+ flush(): Promise<void>;
516
+ }
517
+
518
+ interface QueuedOperation {
519
+ id: string;
520
+ type: 'setMetrics' | 'setUnlockedAchievements' | 'clear';
521
+ data?: any;
522
+ timestamp: number;
523
+ }
524
+ declare class OfflineQueueStorage implements AsyncAchievementStorage {
525
+ private innerStorage;
526
+ private queue;
527
+ private isOnline;
528
+ private isSyncing;
529
+ private queueStorageKey;
530
+ constructor(innerStorage: AsyncAchievementStorage);
531
+ private loadQueue;
532
+ private saveQueue;
533
+ private handleOnline;
534
+ private handleOffline;
535
+ private processQueue;
536
+ private queueOperation;
537
+ getMetrics(): Promise<AchievementMetrics>;
538
+ setMetrics(metrics: AchievementMetrics): Promise<void>;
539
+ getUnlockedAchievements(): Promise<string[]>;
540
+ setUnlockedAchievements(achievements: string[]): Promise<void>;
541
+ clear(): Promise<void>;
542
+ /**
543
+ * Manually trigger queue processing (useful for testing)
544
+ */
545
+ sync(): Promise<void>;
546
+ /**
547
+ * Get current queue status (useful for debugging)
548
+ */
549
+ getQueueStatus(): {
550
+ pending: number;
551
+ operations: QueuedOperation[];
552
+ };
553
+ /**
554
+ * Cleanup listeners (call on unmount)
555
+ */
556
+ destroy(): void;
557
+ }
558
+
559
+ declare function normalizeAchievements(config: AchievementConfigurationType): AchievementConfiguration;
560
+
561
+ /**
562
+ * Exports achievement data to a JSON string
563
+ *
564
+ * @param metrics - Current achievement metrics
565
+ * @param unlocked - Array of unlocked achievement IDs
566
+ * @param configHash - Optional hash of achievement configuration for validation
567
+ * @returns JSON string containing all achievement data
568
+ *
569
+ * @example
570
+ * ```typescript
571
+ * const json = exportAchievementData(_metrics, ['score_100', 'level_5']);
572
+ * // Save json to file or send to server
573
+ * ```
574
+ */
575
+ declare function exportAchievementData(metrics: AchievementMetrics, unlocked: string[], configHash?: string): string;
576
+ /**
577
+ * Creates a simple hash of the achievement configuration
578
+ * Used to validate that imported data matches the current configuration
579
+ *
580
+ * @param config - Achievement configuration object
581
+ * @returns Simple hash string
582
+ */
583
+ declare function createConfigHash(config: any): string;
584
+
585
+ /**
586
+ * Options for importing achievement data
587
+ */
588
+ interface ImportOptions {
589
+ /** Strategy for merging imported data with existing data */
590
+ mergeStrategy?: 'replace' | 'merge' | 'preserve';
591
+ /** Whether to validate the imported data */
592
+ validate?: boolean;
593
+ /** Optional config hash to validate against */
594
+ expectedConfigHash?: string;
595
+ }
596
+ /**
597
+ * Result of an import operation
598
+ */
599
+ interface ImportResult {
600
+ success: boolean;
601
+ imported: {
602
+ metrics: number;
603
+ achievements: number;
604
+ };
605
+ errors?: string[];
606
+ warnings?: string[];
607
+ }
608
+ /**
609
+ * Imports achievement data from a JSON string
610
+ *
611
+ * @param jsonString - JSON string containing exported achievement data
612
+ * @param currentMetrics - Current metrics state
613
+ * @param currentUnlocked - Current unlocked achievements
614
+ * @param options - Import options
615
+ * @returns Import result with success status and any errors
616
+ *
617
+ * @example
618
+ * ```typescript
619
+ * const result = importAchievementData(
620
+ * jsonString,
621
+ * currentMetrics,
622
+ * currentUnlocked,
623
+ * { mergeStrategy: 'merge', validate: true }
624
+ * );
625
+ *
626
+ * if (result.success) {
627
+ * console.log(`Imported ${result.imported.achievements} achievements`);
628
+ * } else {
629
+ * console.error('Import failed:', result.errors);
630
+ * }
631
+ * ```
632
+ */
633
+ declare function importAchievementData(jsonString: string, currentMetrics: AchievementMetrics, currentUnlocked: string[], options?: ImportOptions): ImportResult;
634
+
635
+ export { AchievementCondition, AchievementConfiguration, AchievementConfigurationType, AchievementDetails, AchievementEngine, AchievementEngineApi, AchievementError, AchievementMetricArrayValue, AchievementMetricValue, AchievementMetrics, AchievementState, AchievementStorage, AchievementUnlockedEvent, AchievementWithStatus, AnyAchievementStorage, AsyncAchievementStorage, AsyncStorageAdapter, ConfigurationError, CustomAchievementDetails, EngineConfig, EngineEvent, ErrorEvent, EventEmitter, EventHandler, EventMapping, ImportOptions$1 as ImportOptions, ImportResult$1 as ImportResult, ImportValidationError, IndexedDBStorage, InitialAchievementMetrics, LocalStorage, MemoryStorage, MetricUpdatedEvent, MetricUpdater, OfflineQueueStorage, RestApiStorage, RestApiStorageConfig$1 as RestApiStorageConfig, SimpleAchievementConfig, SimpleAchievementDetails, StateChangedEvent, StorageError, StorageQuotaError, StorageType, SyncError, UnsubscribeFn, createConfigHash, exportAchievementData, importAchievementData, isAchievementError, isAsyncStorage, isRecoverableError, normalizeAchievements };