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/LICENSE +22 -0
- package/README.md +472 -0
- package/dist/index.d.mts +226 -0
- package/dist/index.d.ts +226 -0
- package/dist/index.js +1170 -0
- package/dist/index.mjs +1119 -0
- package/package.json +59 -0
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
|
+
};
|