characterforge-js 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.
package/dist/index.mjs ADDED
@@ -0,0 +1,1119 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/logger.ts
9
+ var LOG_LEVELS = {
10
+ debug: 0,
11
+ info: 1,
12
+ warn: 2,
13
+ error: 3
14
+ };
15
+ var isDevelopment = (() => {
16
+ if (typeof window !== "undefined") {
17
+ return true;
18
+ }
19
+ if (typeof process !== "undefined" && process.env) {
20
+ return process.env.NODE_ENV !== "production";
21
+ }
22
+ return true;
23
+ })();
24
+ var defaultConfig = {
25
+ minLevel: isDevelopment ? "debug" : "warn",
26
+ enableConsole: true
27
+ };
28
+ var Logger = class _Logger {
29
+ constructor(config = {}) {
30
+ this.config = { ...defaultConfig, ...config };
31
+ this.context = config.context;
32
+ }
33
+ /**
34
+ * Create a child logger with additional context
35
+ */
36
+ child(context) {
37
+ return new _Logger({
38
+ ...this.config,
39
+ context: this.context ? `${this.context}:${context}` : context
40
+ });
41
+ }
42
+ /**
43
+ * Check if a log level should be output
44
+ */
45
+ shouldLog(level) {
46
+ return LOG_LEVELS[level] >= LOG_LEVELS[this.config.minLevel];
47
+ }
48
+ /**
49
+ * Format the log entry for console output
50
+ */
51
+ formatConsoleMessage(entry) {
52
+ const parts = [
53
+ `[${entry.timestamp}]`,
54
+ `[${entry.level.toUpperCase()}]`
55
+ ];
56
+ if (entry.context) {
57
+ parts.push(`[${entry.context}]`);
58
+ }
59
+ parts.push(entry.message);
60
+ return parts.join(" ");
61
+ }
62
+ /**
63
+ * Core logging method
64
+ */
65
+ log(level, message, data, error) {
66
+ if (!this.shouldLog(level)) {
67
+ return;
68
+ }
69
+ const entry = {
70
+ level,
71
+ message,
72
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
73
+ context: this.context,
74
+ data,
75
+ error
76
+ };
77
+ if (this.config.enableConsole) {
78
+ const formattedMessage = this.formatConsoleMessage(entry);
79
+ const consoleMethod = level === "debug" ? "log" : level;
80
+ if (data && Object.keys(data).length > 0) {
81
+ console[consoleMethod](formattedMessage, data);
82
+ } else if (error) {
83
+ console[consoleMethod](formattedMessage, error);
84
+ } else {
85
+ console[consoleMethod](formattedMessage);
86
+ }
87
+ }
88
+ }
89
+ /**
90
+ * Debug level logging - for development diagnostics
91
+ */
92
+ debug(message, data) {
93
+ this.log("debug", message, data);
94
+ }
95
+ /**
96
+ * Info level logging - for general information
97
+ */
98
+ info(message, data) {
99
+ this.log("info", message, data);
100
+ }
101
+ /**
102
+ * Warning level logging - for potential issues
103
+ */
104
+ warn(message, data) {
105
+ this.log("warn", message, data);
106
+ }
107
+ /**
108
+ * Error level logging - for errors and exceptions
109
+ */
110
+ error(message, error, data) {
111
+ const errorObj = error instanceof Error ? error : void 0;
112
+ this.log("error", message, data, errorObj);
113
+ }
114
+ /**
115
+ * Log with timing information
116
+ */
117
+ time(label) {
118
+ const start = Date.now();
119
+ this.debug(`Timer started: ${label}`);
120
+ return () => {
121
+ const duration = Date.now() - start;
122
+ this.debug(`Timer ended: ${label}`, { durationMs: duration });
123
+ };
124
+ }
125
+ };
126
+ var logger = new Logger();
127
+ var sdkLogger = logger.child("SDK");
128
+ var apiLogger = logger.child("API");
129
+ var cacheLogger = logger.child("Cache");
130
+
131
+ // src/cache/web.ts
132
+ var DB_NAME = "CharacterForgeDB";
133
+ var STORE_NAME = "images";
134
+ var DB_VERSION = 2;
135
+ var MAX_CACHE_SIZE = 100;
136
+ var CACHE_EXPIRY_MS = 7 * 24 * 60 * 60 * 1e3;
137
+ var ObjectURLManager = class {
138
+ constructor() {
139
+ this.urls = /* @__PURE__ */ new Map();
140
+ }
141
+ create(key, blob) {
142
+ this.revoke(key);
143
+ const url = URL.createObjectURL(blob);
144
+ this.urls.set(key, url);
145
+ return url;
146
+ }
147
+ revoke(key) {
148
+ const url = this.urls.get(key);
149
+ if (url) {
150
+ URL.revokeObjectURL(url);
151
+ this.urls.delete(key);
152
+ }
153
+ }
154
+ revokeAll() {
155
+ this.urls.forEach((url) => URL.revokeObjectURL(url));
156
+ this.urls.clear();
157
+ }
158
+ get(key) {
159
+ return this.urls.get(key);
160
+ }
161
+ };
162
+ var WebCacheManager = class {
163
+ constructor() {
164
+ this.dbPromise = null;
165
+ this.urlManager = new ObjectURLManager();
166
+ this.cleanupIntervalId = null;
167
+ this.isSupported = typeof window !== "undefined" && !!window.indexedDB;
168
+ if (this.isSupported) {
169
+ this.dbPromise = this.openDB();
170
+ this.scheduleCleanup();
171
+ }
172
+ }
173
+ openDB() {
174
+ return new Promise((resolve, reject) => {
175
+ if (!window.indexedDB) {
176
+ reject(new Error("IndexedDB not supported"));
177
+ return;
178
+ }
179
+ const request = window.indexedDB.open(DB_NAME, DB_VERSION);
180
+ request.onerror = () => {
181
+ cacheLogger.error("Failed to open database", request.error);
182
+ reject(request.error);
183
+ };
184
+ request.onsuccess = () => resolve(request.result);
185
+ request.onupgradeneeded = (event) => {
186
+ const db = event.target.result;
187
+ if (db.objectStoreNames.contains(STORE_NAME)) {
188
+ db.deleteObjectStore(STORE_NAME);
189
+ }
190
+ db.createObjectStore(STORE_NAME);
191
+ };
192
+ });
193
+ }
194
+ async get(key) {
195
+ if (!this.isSupported || !this.dbPromise) {
196
+ return null;
197
+ }
198
+ const existingUrl = this.urlManager.get(key);
199
+ if (existingUrl) {
200
+ this.updateAccessTime(key).catch(() => {
201
+ });
202
+ return existingUrl;
203
+ }
204
+ try {
205
+ const db = await this.dbPromise;
206
+ const entry = await this.getEntry(db, key);
207
+ if (!entry) {
208
+ return null;
209
+ }
210
+ if (Date.now() - entry.createdAt > CACHE_EXPIRY_MS) {
211
+ await this.delete(key);
212
+ return null;
213
+ }
214
+ const url = this.urlManager.create(key, entry.blob);
215
+ this.updateAccessTime(key).catch(() => {
216
+ });
217
+ return url;
218
+ } catch (error) {
219
+ cacheLogger.warn("Cache retrieval failed", { error });
220
+ return null;
221
+ }
222
+ }
223
+ async set(key, data) {
224
+ if (!this.isSupported || !this.dbPromise) {
225
+ if (typeof data === "string") return data;
226
+ return URL.createObjectURL(data);
227
+ }
228
+ try {
229
+ const db = await this.dbPromise;
230
+ let blob;
231
+ if (typeof data === "string") {
232
+ const response = await fetch(data);
233
+ if (!response.ok) {
234
+ throw new Error(`Failed to fetch image: ${response.status}`);
235
+ }
236
+ blob = await response.blob();
237
+ } else {
238
+ blob = data;
239
+ }
240
+ const entry = {
241
+ blob,
242
+ createdAt: Date.now(),
243
+ accessedAt: Date.now()
244
+ };
245
+ await this.putEntry(db, key, entry);
246
+ this.enforceLimit(db).catch(() => {
247
+ });
248
+ return this.urlManager.create(key, blob);
249
+ } catch (error) {
250
+ cacheLogger.warn("Cache storage failed", { error });
251
+ if (typeof data === "string") return data;
252
+ return URL.createObjectURL(data);
253
+ }
254
+ }
255
+ async delete(key) {
256
+ this.urlManager.revoke(key);
257
+ if (!this.isSupported || !this.dbPromise) return;
258
+ try {
259
+ const db = await this.dbPromise;
260
+ await this.deleteEntry(db, key);
261
+ } catch (error) {
262
+ cacheLogger.warn("Cache deletion failed", { error });
263
+ }
264
+ }
265
+ async clear() {
266
+ this.urlManager.revokeAll();
267
+ if (!this.isSupported || !this.dbPromise) return;
268
+ try {
269
+ const db = await this.dbPromise;
270
+ await new Promise((resolve, reject) => {
271
+ const transaction = db.transaction(STORE_NAME, "readwrite");
272
+ const store = transaction.objectStore(STORE_NAME);
273
+ const request = store.clear();
274
+ request.onerror = () => reject(request.error);
275
+ request.onsuccess = () => resolve();
276
+ });
277
+ } catch (error) {
278
+ cacheLogger.warn("Cache clear failed", { error });
279
+ }
280
+ }
281
+ /**
282
+ * Destroy the cache manager and clean up resources
283
+ * Call this when you no longer need the cache instance
284
+ */
285
+ destroy() {
286
+ if (this.cleanupIntervalId !== null) {
287
+ clearInterval(this.cleanupIntervalId);
288
+ this.cleanupIntervalId = null;
289
+ }
290
+ this.urlManager.revokeAll();
291
+ }
292
+ // =============================================================================
293
+ // Private Helpers
294
+ // =============================================================================
295
+ async getEntry(db, key) {
296
+ return new Promise((resolve, reject) => {
297
+ const transaction = db.transaction(STORE_NAME, "readonly");
298
+ const store = transaction.objectStore(STORE_NAME);
299
+ const request = store.get(key);
300
+ request.onerror = () => reject(request.error);
301
+ request.onsuccess = () => resolve(request.result || null);
302
+ });
303
+ }
304
+ async putEntry(db, key, entry) {
305
+ return new Promise((resolve, reject) => {
306
+ const transaction = db.transaction(STORE_NAME, "readwrite");
307
+ const store = transaction.objectStore(STORE_NAME);
308
+ const request = store.put(entry, key);
309
+ request.onerror = () => reject(request.error);
310
+ request.onsuccess = () => resolve();
311
+ });
312
+ }
313
+ async deleteEntry(db, key) {
314
+ return new Promise((resolve, reject) => {
315
+ const transaction = db.transaction(STORE_NAME, "readwrite");
316
+ const store = transaction.objectStore(STORE_NAME);
317
+ const request = store.delete(key);
318
+ request.onerror = () => reject(request.error);
319
+ request.onsuccess = () => resolve();
320
+ });
321
+ }
322
+ async updateAccessTime(key) {
323
+ if (!this.dbPromise) return;
324
+ try {
325
+ const db = await this.dbPromise;
326
+ const entry = await this.getEntry(db, key);
327
+ if (entry) {
328
+ entry.accessedAt = Date.now();
329
+ await this.putEntry(db, key, entry);
330
+ }
331
+ } catch {
332
+ }
333
+ }
334
+ async enforceLimit(db) {
335
+ try {
336
+ const count = await this.getCount(db);
337
+ if (count > MAX_CACHE_SIZE) {
338
+ const keysToDelete = await this.getOldestKeys(db, count - MAX_CACHE_SIZE);
339
+ for (const key of keysToDelete) {
340
+ this.urlManager.revoke(key);
341
+ await this.deleteEntry(db, key);
342
+ }
343
+ }
344
+ } catch (error) {
345
+ cacheLogger.warn("Failed to enforce cache limit", { error });
346
+ }
347
+ }
348
+ async getCount(db) {
349
+ return new Promise((resolve, reject) => {
350
+ const transaction = db.transaction(STORE_NAME, "readonly");
351
+ const store = transaction.objectStore(STORE_NAME);
352
+ const request = store.count();
353
+ request.onerror = () => reject(request.error);
354
+ request.onsuccess = () => resolve(request.result);
355
+ });
356
+ }
357
+ async getOldestKeys(db, count) {
358
+ return new Promise((resolve, reject) => {
359
+ const transaction = db.transaction(STORE_NAME, "readonly");
360
+ const store = transaction.objectStore(STORE_NAME);
361
+ const entries = [];
362
+ const request = store.openCursor();
363
+ request.onerror = () => reject(request.error);
364
+ request.onsuccess = (event) => {
365
+ const cursor = event.target.result;
366
+ if (cursor) {
367
+ const entry = cursor.value;
368
+ entries.push({
369
+ key: cursor.key,
370
+ accessedAt: entry.accessedAt
371
+ });
372
+ cursor.continue();
373
+ } else {
374
+ entries.sort((a, b) => a.accessedAt - b.accessedAt);
375
+ const oldestKeys = entries.slice(0, count).map((e) => e.key);
376
+ resolve(oldestKeys);
377
+ }
378
+ };
379
+ });
380
+ }
381
+ scheduleCleanup() {
382
+ if (this.cleanupIntervalId !== null) {
383
+ clearInterval(this.cleanupIntervalId);
384
+ }
385
+ this.cleanupIntervalId = setInterval(() => this.cleanup().catch(() => {
386
+ }), 60 * 60 * 1e3);
387
+ }
388
+ async cleanup() {
389
+ if (!this.dbPromise) return;
390
+ try {
391
+ const db = await this.dbPromise;
392
+ const expiredKeys = [];
393
+ await new Promise((resolve, reject) => {
394
+ const transaction = db.transaction(STORE_NAME, "readonly");
395
+ const store = transaction.objectStore(STORE_NAME);
396
+ const request = store.openCursor();
397
+ request.onerror = () => reject(request.error);
398
+ request.onsuccess = (event) => {
399
+ const cursor = event.target.result;
400
+ if (cursor) {
401
+ const entry = cursor.value;
402
+ if (Date.now() - entry.createdAt > CACHE_EXPIRY_MS) {
403
+ expiredKeys.push(cursor.key);
404
+ }
405
+ cursor.continue();
406
+ } else {
407
+ resolve();
408
+ }
409
+ };
410
+ });
411
+ for (const key of expiredKeys) {
412
+ await this.delete(key);
413
+ }
414
+ if (expiredKeys.length > 0) {
415
+ cacheLogger.info(`Cleaned up ${expiredKeys.length} expired entries`);
416
+ }
417
+ } catch (error) {
418
+ cacheLogger.warn("Cleanup failed", { error });
419
+ }
420
+ }
421
+ };
422
+
423
+ // src/cache/native.ts
424
+ var CACHE_DIR = "character-forge-cache/";
425
+ var MAX_CACHE_SIZE2 = 100;
426
+ var CACHE_EXPIRY_MS2 = 7 * 24 * 60 * 60 * 1e3;
427
+ var METADATA_KEY = "@characterforge:cache-metadata";
428
+ function getFileSystemAdapter() {
429
+ try {
430
+ const ExpoFileSystem = __require("expo-file-system");
431
+ if (ExpoFileSystem?.documentDirectory) {
432
+ return {
433
+ documentDirectory: ExpoFileSystem.documentDirectory,
434
+ downloadAsync: ExpoFileSystem.downloadAsync,
435
+ writeAsStringAsync: ExpoFileSystem.writeAsStringAsync,
436
+ readAsStringAsync: ExpoFileSystem.readAsStringAsync,
437
+ getInfoAsync: ExpoFileSystem.getInfoAsync,
438
+ makeDirectoryAsync: ExpoFileSystem.makeDirectoryAsync,
439
+ deleteAsync: ExpoFileSystem.deleteAsync,
440
+ readDirectoryAsync: ExpoFileSystem.readDirectoryAsync
441
+ };
442
+ }
443
+ } catch {
444
+ }
445
+ try {
446
+ const RNFS = __require("react-native-fs");
447
+ if (RNFS?.DocumentDirectoryPath) {
448
+ return {
449
+ documentDirectory: RNFS.DocumentDirectoryPath + "/",
450
+ downloadAsync: async (url, fileUri) => {
451
+ await RNFS.downloadFile({ fromUrl: url, toFile: fileUri }).promise;
452
+ },
453
+ writeAsStringAsync: async (fileUri, contents) => {
454
+ await RNFS.writeFile(fileUri, contents, "utf8");
455
+ },
456
+ readAsStringAsync: async (fileUri) => {
457
+ return await RNFS.readFile(fileUri, "utf8");
458
+ },
459
+ getInfoAsync: async (fileUri) => {
460
+ const exists = await RNFS.exists(fileUri);
461
+ if (exists) {
462
+ const stat = await RNFS.stat(fileUri);
463
+ return { exists: true, size: stat.size };
464
+ }
465
+ return { exists: false };
466
+ },
467
+ makeDirectoryAsync: async (dirUri) => {
468
+ await RNFS.mkdir(dirUri);
469
+ },
470
+ deleteAsync: async (fileUri) => {
471
+ const exists = await RNFS.exists(fileUri);
472
+ if (exists) {
473
+ await RNFS.unlink(fileUri);
474
+ }
475
+ },
476
+ readDirectoryAsync: async (dirUri) => {
477
+ const files = await RNFS.readDir(dirUri);
478
+ return files.map((file) => file.name);
479
+ }
480
+ };
481
+ }
482
+ } catch {
483
+ }
484
+ return null;
485
+ }
486
+ function getAsyncStorage() {
487
+ try {
488
+ return __require("@react-native-async-storage/async-storage").default;
489
+ } catch {
490
+ try {
491
+ return __require("react-native").AsyncStorage;
492
+ } catch {
493
+ return null;
494
+ }
495
+ }
496
+ }
497
+ var NativeCacheManager = class {
498
+ constructor() {
499
+ this.metadata = {};
500
+ this.cleanupIntervalId = null;
501
+ this.fs = getFileSystemAdapter();
502
+ this.asyncStorage = getAsyncStorage();
503
+ this.isSupported = !!(this.fs && this.asyncStorage);
504
+ if (this.isSupported && this.fs) {
505
+ this.cacheDir = this.fs.documentDirectory + CACHE_DIR;
506
+ this.initPromise = this.initialize();
507
+ } else {
508
+ this.cacheDir = "";
509
+ this.initPromise = Promise.resolve();
510
+ cacheLogger.warn(
511
+ "React Native cache not supported. Install expo-file-system or react-native-fs and @react-native-async-storage/async-storage."
512
+ );
513
+ }
514
+ }
515
+ async initialize() {
516
+ if (!this.fs) return;
517
+ try {
518
+ const dirInfo = await this.fs.getInfoAsync(this.cacheDir);
519
+ if (!dirInfo.exists) {
520
+ await this.fs.makeDirectoryAsync(this.cacheDir, { intermediates: true });
521
+ }
522
+ await this.loadMetadata();
523
+ this.scheduleCleanup();
524
+ } catch (error) {
525
+ cacheLogger.error("Failed to initialize cache", error);
526
+ }
527
+ }
528
+ async loadMetadata() {
529
+ if (!this.asyncStorage) return;
530
+ try {
531
+ const data = await this.asyncStorage.getItem(METADATA_KEY);
532
+ if (data) {
533
+ this.metadata = JSON.parse(data);
534
+ }
535
+ } catch (error) {
536
+ cacheLogger.warn("Failed to load cache metadata", { error });
537
+ this.metadata = {};
538
+ }
539
+ }
540
+ async saveMetadata() {
541
+ if (!this.asyncStorage) return;
542
+ try {
543
+ await this.asyncStorage.setItem(METADATA_KEY, JSON.stringify(this.metadata));
544
+ } catch (error) {
545
+ cacheLogger.warn("Failed to save cache metadata", { error });
546
+ }
547
+ }
548
+ async get(key) {
549
+ if (!this.isSupported || !this.fs) {
550
+ return null;
551
+ }
552
+ await this.initPromise;
553
+ const entry = this.metadata[key];
554
+ if (!entry) {
555
+ return null;
556
+ }
557
+ if (Date.now() - entry.createdAt > CACHE_EXPIRY_MS2) {
558
+ await this.delete(key);
559
+ return null;
560
+ }
561
+ const fileUri = this.cacheDir + entry.fileName;
562
+ const fileInfo = await this.fs.getInfoAsync(fileUri);
563
+ if (!fileInfo.exists) {
564
+ delete this.metadata[key];
565
+ await this.saveMetadata();
566
+ return null;
567
+ }
568
+ entry.accessedAt = Date.now();
569
+ await this.saveMetadata();
570
+ return fileUri;
571
+ }
572
+ async set(key, data) {
573
+ if (!this.isSupported || !this.fs) {
574
+ if (typeof data === "string") return data;
575
+ throw new Error("Cache not supported in this environment");
576
+ }
577
+ await this.initPromise;
578
+ if (typeof data !== "string") {
579
+ cacheLogger.warn("Blob caching not supported in React Native. Use URL strings instead.");
580
+ throw new Error("Blob caching not supported in React Native. Please provide a URL string instead.");
581
+ }
582
+ try {
583
+ const fileName = `${Date.now()}-${Math.random().toString(36).substring(7)}.png`;
584
+ const fileUri = this.cacheDir + fileName;
585
+ await this.fs.downloadAsync(data, fileUri);
586
+ this.metadata[key] = {
587
+ fileName,
588
+ createdAt: Date.now(),
589
+ accessedAt: Date.now()
590
+ };
591
+ await this.saveMetadata();
592
+ await this.enforceLimit();
593
+ return fileUri;
594
+ } catch (error) {
595
+ cacheLogger.warn("Cache storage failed", { error });
596
+ return data;
597
+ }
598
+ }
599
+ async delete(key) {
600
+ if (!this.isSupported || !this.fs) return;
601
+ await this.initPromise;
602
+ const entry = this.metadata[key];
603
+ if (!entry) return;
604
+ try {
605
+ const fileUri = this.cacheDir + entry.fileName;
606
+ await this.fs.deleteAsync(fileUri, { idempotent: true });
607
+ } catch (error) {
608
+ cacheLogger.warn("Failed to delete cache file", { error });
609
+ }
610
+ delete this.metadata[key];
611
+ await this.saveMetadata();
612
+ }
613
+ async clear() {
614
+ if (!this.isSupported || !this.fs) return;
615
+ await this.initPromise;
616
+ try {
617
+ const files = await this.fs.readDirectoryAsync(this.cacheDir);
618
+ for (const file of files) {
619
+ await this.fs.deleteAsync(this.cacheDir + file, { idempotent: true });
620
+ }
621
+ this.metadata = {};
622
+ await this.saveMetadata();
623
+ } catch (error) {
624
+ cacheLogger.warn("Cache clear failed", { error });
625
+ }
626
+ }
627
+ /**
628
+ * Destroy the cache manager and clean up resources
629
+ * Call this when you no longer need the cache instance
630
+ */
631
+ destroy() {
632
+ if (this.cleanupIntervalId !== null) {
633
+ clearInterval(this.cleanupIntervalId);
634
+ this.cleanupIntervalId = null;
635
+ }
636
+ }
637
+ // =============================================================================
638
+ // Private Helpers
639
+ // =============================================================================
640
+ async enforceLimit() {
641
+ const keys = Object.keys(this.metadata);
642
+ if (keys.length <= MAX_CACHE_SIZE2) return;
643
+ const sortedKeys = keys.sort((a, b) => {
644
+ return this.metadata[a].accessedAt - this.metadata[b].accessedAt;
645
+ });
646
+ const toDelete = sortedKeys.slice(0, keys.length - MAX_CACHE_SIZE2);
647
+ for (const key of toDelete) {
648
+ await this.delete(key);
649
+ }
650
+ }
651
+ scheduleCleanup() {
652
+ if (this.cleanupIntervalId !== null) {
653
+ clearInterval(this.cleanupIntervalId);
654
+ }
655
+ this.cleanupIntervalId = setInterval(() => this.cleanup().catch(() => {
656
+ }), 60 * 60 * 1e3);
657
+ }
658
+ async cleanup() {
659
+ if (!this.isSupported) return;
660
+ const now = Date.now();
661
+ const expiredKeys = Object.keys(this.metadata).filter(
662
+ (key) => now - this.metadata[key].createdAt > CACHE_EXPIRY_MS2
663
+ );
664
+ for (const key of expiredKeys) {
665
+ await this.delete(key);
666
+ }
667
+ if (expiredKeys.length > 0) {
668
+ cacheLogger.info(`Cleaned up ${expiredKeys.length} expired entries`);
669
+ }
670
+ }
671
+ };
672
+
673
+ // src/cache/index.ts
674
+ function isReactNative() {
675
+ return typeof navigator !== "undefined" && navigator.product === "ReactNative";
676
+ }
677
+ function isBrowser() {
678
+ return typeof window !== "undefined" && typeof window.document !== "undefined";
679
+ }
680
+ function createCacheManager() {
681
+ if (isReactNative()) {
682
+ cacheLogger.debug("Creating React Native cache manager");
683
+ return new NativeCacheManager();
684
+ }
685
+ if (isBrowser()) {
686
+ cacheLogger.debug("Creating Web cache manager");
687
+ return new WebCacheManager();
688
+ }
689
+ cacheLogger.warn("Platform not supported, using no-op cache");
690
+ return new NoOpCacheManager();
691
+ }
692
+ var NoOpCacheManager = class {
693
+ async get(_key) {
694
+ return null;
695
+ }
696
+ async set(_key, data) {
697
+ if (typeof data === "string") {
698
+ return data;
699
+ }
700
+ throw new Error("Cache not supported in this environment");
701
+ }
702
+ async clear() {
703
+ }
704
+ async delete(_key) {
705
+ }
706
+ destroy() {
707
+ }
708
+ };
709
+
710
+ // src/errors.ts
711
+ var AppError = class extends Error {
712
+ constructor(message, code = "APP_ERROR", statusCode = 500, isOperational = true) {
713
+ super(message);
714
+ this.name = this.constructor.name;
715
+ this.code = code;
716
+ this.statusCode = statusCode;
717
+ this.isOperational = isOperational;
718
+ this.timestamp = /* @__PURE__ */ new Date();
719
+ if (Error.captureStackTrace) {
720
+ Error.captureStackTrace(this, this.constructor);
721
+ }
722
+ }
723
+ toJSON() {
724
+ return {
725
+ name: this.name,
726
+ message: this.message,
727
+ code: this.code,
728
+ statusCode: this.statusCode,
729
+ timestamp: this.timestamp.toISOString()
730
+ };
731
+ }
732
+ };
733
+ var AuthenticationError = class extends AppError {
734
+ constructor(message = "Invalid or missing API key") {
735
+ super(message, "AUTH_ERROR", 401);
736
+ }
737
+ };
738
+ var AuthorizationError = class extends AppError {
739
+ constructor(message = "Not authorized to perform this action") {
740
+ super(message, "AUTHORIZATION_ERROR", 403);
741
+ }
742
+ };
743
+ var InsufficientCreditsError = class extends AppError {
744
+ constructor(required = 1, available = 0) {
745
+ super(
746
+ "Insufficient credits. Please purchase more credits to continue.",
747
+ "INSUFFICIENT_CREDITS",
748
+ 402
749
+ );
750
+ this.required = required;
751
+ this.available = available;
752
+ }
753
+ };
754
+ var PaymentError = class extends AppError {
755
+ constructor(message = "Payment processing failed") {
756
+ super(message, "PAYMENT_ERROR", 402);
757
+ }
758
+ };
759
+ var ApiError = class extends AppError {
760
+ constructor(message, statusCode = 500, endpoint, code = "API_ERROR") {
761
+ super(message, code, statusCode);
762
+ this.endpoint = endpoint;
763
+ }
764
+ };
765
+ var RateLimitError = class extends ApiError {
766
+ constructor(retryAfter) {
767
+ super("Too many requests. Please try again later.", 429, void 0, "RATE_LIMIT");
768
+ this.retryAfter = retryAfter;
769
+ }
770
+ };
771
+ var NetworkError = class extends AppError {
772
+ constructor(message = "Network error. Please check your connection.") {
773
+ super(message, "NETWORK_ERROR", 0);
774
+ }
775
+ };
776
+ var GenerationError = class extends AppError {
777
+ constructor(message = "Failed to generate character", code = "GENERATION_ERROR") {
778
+ super(message, code, 500);
779
+ }
780
+ };
781
+ var ImageProcessingError = class extends GenerationError {
782
+ constructor(message = "Failed to process image") {
783
+ super(message, "IMAGE_PROCESSING_ERROR");
784
+ }
785
+ };
786
+ var ValidationError = class extends AppError {
787
+ constructor(message, field, value, code = "VALIDATION_ERROR") {
788
+ super(message, code, 400);
789
+ this.field = field;
790
+ this.value = value;
791
+ }
792
+ };
793
+ var ConfigValidationError = class extends ValidationError {
794
+ constructor(message, field) {
795
+ super(message, field, void 0, "CONFIG_VALIDATION_ERROR");
796
+ }
797
+ };
798
+ var CacheError = class extends AppError {
799
+ constructor(message = "Cache operation failed") {
800
+ super(message, "CACHE_ERROR", 500);
801
+ }
802
+ };
803
+ function isAppError(error) {
804
+ return error instanceof AppError;
805
+ }
806
+ function isAuthenticationError(error) {
807
+ return error instanceof AuthenticationError;
808
+ }
809
+ function isInsufficientCreditsError(error) {
810
+ return error instanceof InsufficientCreditsError;
811
+ }
812
+ function isNetworkError(error) {
813
+ return error instanceof NetworkError;
814
+ }
815
+ function isRateLimitError(error) {
816
+ return error instanceof RateLimitError;
817
+ }
818
+ function parseError(error) {
819
+ if (isAppError(error)) {
820
+ return error;
821
+ }
822
+ if (error instanceof Error) {
823
+ const message = error.message.toLowerCase();
824
+ if (message.includes("api key") || message.includes("authentication")) {
825
+ return new AuthenticationError(error.message);
826
+ }
827
+ if (message.includes("credits") || message.includes("insufficient")) {
828
+ return new InsufficientCreditsError();
829
+ }
830
+ if (message.includes("network") || message.includes("fetch")) {
831
+ return new NetworkError(error.message);
832
+ }
833
+ return new AppError(error.message);
834
+ }
835
+ if (typeof error === "string") {
836
+ return new AppError(error);
837
+ }
838
+ return new AppError("An unexpected error occurred");
839
+ }
840
+ function getUserFriendlyMessage(error) {
841
+ const appError = parseError(error);
842
+ const friendlyMessages = {
843
+ AUTH_ERROR: "Invalid API key. Please check your credentials.",
844
+ INSUFFICIENT_CREDITS: "You need more credits. Purchase credits to continue.",
845
+ RATE_LIMIT: "You're doing that too fast. Please wait a moment.",
846
+ NETWORK_ERROR: "Connection problem. Please check your internet.",
847
+ GENERATION_ERROR: "Unable to create your character. Please try again."
848
+ };
849
+ return friendlyMessages[appError.code] || appError.message;
850
+ }
851
+
852
+ // src/client.ts
853
+ var DEFAULT_BASE_URL = "https://mnxzykltetirdcnxugcl.supabase.co/functions/v1";
854
+ var DEFAULT_TIMEOUT = 6e4;
855
+ var DEFAULT_RETRY_CONFIG = {
856
+ maxRetries: 3,
857
+ baseDelayMs: 1e3,
858
+ maxDelayMs: 1e4
859
+ };
860
+ var RETRYABLE_STATUS_CODES = [408, 429, 500, 502, 503, 504];
861
+ function generateCacheKey(config) {
862
+ const sortedKeys = Object.keys(config).sort();
863
+ const stableObj = {};
864
+ for (const key of sortedKeys) {
865
+ if (key === "cache") continue;
866
+ const value = config[key];
867
+ if (value !== void 0) {
868
+ stableObj[key] = value;
869
+ }
870
+ }
871
+ return JSON.stringify(stableObj);
872
+ }
873
+ function calculateBackoffDelay(attempt, baseDelay, maxDelay) {
874
+ const exponentialDelay = baseDelay * Math.pow(2, attempt);
875
+ const jitter = Math.random() * 0.3 * exponentialDelay;
876
+ return Math.min(exponentialDelay + jitter, maxDelay);
877
+ }
878
+ function delay(ms) {
879
+ return new Promise((resolve) => setTimeout(resolve, ms));
880
+ }
881
+ function isRetryableError(error) {
882
+ if (error instanceof NetworkError) return true;
883
+ if (error instanceof RateLimitError) return true;
884
+ if (error instanceof ApiError) return true;
885
+ if (error instanceof Error) {
886
+ const message = error.message.toLowerCase();
887
+ return message.includes("network") || message.includes("timeout") || message.includes("fetch") || message.includes("failed to fetch");
888
+ }
889
+ return false;
890
+ }
891
+ function parseHttpError(status, data) {
892
+ const errorData = data;
893
+ const message = errorData.error || "An unexpected error occurred";
894
+ if (status === 401 || message.toLowerCase().includes("api key")) {
895
+ return new AuthenticationError(message);
896
+ }
897
+ if (status === 402 || message.toLowerCase().includes("credits")) {
898
+ return new InsufficientCreditsError();
899
+ }
900
+ if (status === 429) {
901
+ return new RateLimitError();
902
+ }
903
+ if (RETRYABLE_STATUS_CODES.includes(status)) {
904
+ return new ApiError(message, status, "generate-character");
905
+ }
906
+ return new GenerationError(message);
907
+ }
908
+ async function fetchWithTimeout(url, options, timeoutMs) {
909
+ const controller = new AbortController();
910
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
911
+ try {
912
+ const response = await fetch(url, {
913
+ ...options,
914
+ signal: controller.signal
915
+ });
916
+ clearTimeout(timeoutId);
917
+ return response;
918
+ } catch (error) {
919
+ clearTimeout(timeoutId);
920
+ if (error instanceof Error && error.name === "AbortError") {
921
+ throw new NetworkError("Request timeout");
922
+ }
923
+ throw error;
924
+ }
925
+ }
926
+ var CharacterForgeClient = class {
927
+ constructor(config) {
928
+ if (!config.apiKey) {
929
+ throw new AuthenticationError("API key is required");
930
+ }
931
+ this.config = {
932
+ apiKey: config.apiKey,
933
+ baseUrl: config.baseUrl || DEFAULT_BASE_URL,
934
+ cache: config.cache ?? true,
935
+ cacheManager: config.cacheManager || createCacheManager(),
936
+ timeout: config.timeout || DEFAULT_TIMEOUT,
937
+ retry: config.retry || DEFAULT_RETRY_CONFIG
938
+ };
939
+ this.retryConfig = this.config.retry;
940
+ this.cacheManager = this.config.cacheManager;
941
+ sdkLogger.info("SDK Client initialized", {
942
+ cacheEnabled: this.config.cache,
943
+ baseUrl: this.config.baseUrl
944
+ });
945
+ }
946
+ /**
947
+ * Generate a character image based on the configuration
948
+ * Includes caching, retry logic, and comprehensive error handling
949
+ */
950
+ async generate(characterConfig, onStatusUpdate) {
951
+ const shouldCache = this.config.cache && characterConfig.cache !== false;
952
+ const cacheKey = generateCacheKey(characterConfig);
953
+ sdkLogger.debug("Starting generation", {
954
+ shouldCache,
955
+ gender: characterConfig.gender
956
+ });
957
+ if (shouldCache) {
958
+ try {
959
+ const cachedUrl = await this.cacheManager.get(cacheKey);
960
+ if (cachedUrl) {
961
+ sdkLogger.info("Cache hit");
962
+ onStatusUpdate?.("Retrieved from Client Cache!");
963
+ return cachedUrl;
964
+ }
965
+ sdkLogger.debug("Cache miss");
966
+ } catch (cacheError) {
967
+ sdkLogger.warn("Cache lookup failed", { error: cacheError });
968
+ }
969
+ }
970
+ onStatusUpdate?.("Calling AI Cloud...");
971
+ const imageUrl = await this.callApiWithRetry(characterConfig);
972
+ if (shouldCache && imageUrl) {
973
+ const cachedUrl = await this.cacheResult(cacheKey, imageUrl, onStatusUpdate);
974
+ if (cachedUrl) {
975
+ sdkLogger.debug("Returning local cached URL");
976
+ return cachedUrl;
977
+ }
978
+ }
979
+ return imageUrl;
980
+ }
981
+ /**
982
+ * Call the generation API with retry logic
983
+ */
984
+ async callApiWithRetry(config) {
985
+ let lastError = null;
986
+ for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
987
+ try {
988
+ sdkLogger.debug("API call attempt", { attempt: attempt + 1 });
989
+ const url = `${this.config.baseUrl}/generate-character`;
990
+ const response = await fetchWithTimeout(
991
+ url,
992
+ {
993
+ method: "POST",
994
+ headers: {
995
+ "Content-Type": "application/json",
996
+ "Authorization": `Bearer ${this.config.apiKey}`
997
+ },
998
+ body: JSON.stringify(config)
999
+ },
1000
+ this.config.timeout
1001
+ );
1002
+ if (!response.ok) {
1003
+ const data2 = await response.json();
1004
+ const error = parseHttpError(response.status, data2);
1005
+ throw error;
1006
+ }
1007
+ const data = await response.json();
1008
+ if (!data.image) {
1009
+ throw new GenerationError("No image URL in response");
1010
+ }
1011
+ sdkLogger.info("Generation successful", {
1012
+ attempt: attempt + 1
1013
+ });
1014
+ return data.image;
1015
+ } catch (error) {
1016
+ lastError = error instanceof Error ? error : new GenerationError("Unknown error");
1017
+ if (lastError instanceof AuthenticationError || lastError instanceof InsufficientCreditsError) {
1018
+ throw lastError;
1019
+ }
1020
+ if (attempt < this.retryConfig.maxRetries && isRetryableError(error)) {
1021
+ const delayMs = calculateBackoffDelay(
1022
+ attempt,
1023
+ this.retryConfig.baseDelayMs,
1024
+ this.retryConfig.maxDelayMs
1025
+ );
1026
+ sdkLogger.warn("Retrying after error", {
1027
+ attempt: attempt + 1,
1028
+ delayMs,
1029
+ error: lastError.message
1030
+ });
1031
+ await delay(delayMs);
1032
+ continue;
1033
+ }
1034
+ throw lastError;
1035
+ }
1036
+ }
1037
+ throw lastError || new GenerationError("Generation failed after retries");
1038
+ }
1039
+ /**
1040
+ * Cache the generation result
1041
+ */
1042
+ async cacheResult(cacheKey, imageUrl, onStatusUpdate) {
1043
+ try {
1044
+ onStatusUpdate?.("Caching result...");
1045
+ await this.cacheManager.set(cacheKey, imageUrl);
1046
+ const cachedUrl = await this.cacheManager.get(cacheKey);
1047
+ if (cachedUrl) {
1048
+ sdkLogger.debug("Result cached successfully");
1049
+ return cachedUrl;
1050
+ }
1051
+ } catch (cacheError) {
1052
+ sdkLogger.warn("Failed to cache image", { error: cacheError });
1053
+ }
1054
+ return null;
1055
+ }
1056
+ /**
1057
+ * Clear the local cache
1058
+ */
1059
+ async clearCache() {
1060
+ sdkLogger.info("Clearing cache");
1061
+ await this.cacheManager.clear();
1062
+ }
1063
+ /**
1064
+ * Get cache statistics (if supported by cache manager)
1065
+ */
1066
+ async getCacheStats() {
1067
+ return null;
1068
+ }
1069
+ /**
1070
+ * Destroy the client and clean up resources
1071
+ * Call this when you no longer need the client instance
1072
+ */
1073
+ destroy() {
1074
+ sdkLogger.info("Destroying SDK client");
1075
+ if (this.cacheManager && "destroy" in this.cacheManager && typeof this.cacheManager.destroy === "function") {
1076
+ this.cacheManager.destroy();
1077
+ }
1078
+ }
1079
+ };
1080
+ function createCharacterForgeClient(config) {
1081
+ return new CharacterForgeClient(config);
1082
+ }
1083
+
1084
+ // src/index.ts
1085
+ var VERSION = "1.0.0";
1086
+ export {
1087
+ ApiError,
1088
+ AppError,
1089
+ AuthenticationError,
1090
+ AuthorizationError,
1091
+ CacheError,
1092
+ CharacterForgeClient,
1093
+ GenerationError as CharacterForgeError,
1094
+ ConfigValidationError,
1095
+ GenerationError,
1096
+ ImageProcessingError,
1097
+ InsufficientCreditsError,
1098
+ Logger,
1099
+ NativeCacheManager,
1100
+ NetworkError,
1101
+ PaymentError,
1102
+ RateLimitError,
1103
+ VERSION,
1104
+ ValidationError,
1105
+ WebCacheManager,
1106
+ createCacheManager,
1107
+ createCharacterForgeClient,
1108
+ getUserFriendlyMessage,
1109
+ isAppError,
1110
+ isAuthenticationError,
1111
+ isBrowser,
1112
+ isInsufficientCreditsError,
1113
+ isNetworkError,
1114
+ isRateLimitError,
1115
+ isReactNative,
1116
+ logger,
1117
+ parseError,
1118
+ sdkLogger
1119
+ };