@axyle/expo-sdk 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.
Files changed (43) hide show
  1. package/CONFIG.md +117 -0
  2. package/README.md +534 -0
  3. package/dist/client.d.ts +93 -0
  4. package/dist/client.js +416 -0
  5. package/dist/config.example.d.ts +8 -0
  6. package/dist/config.example.js +13 -0
  7. package/dist/configLoader.d.ts +10 -0
  8. package/dist/configLoader.js +22 -0
  9. package/dist/constants.d.ts +38 -0
  10. package/dist/constants.js +42 -0
  11. package/dist/context.d.ts +36 -0
  12. package/dist/context.js +216 -0
  13. package/dist/core.d.ts +9 -0
  14. package/dist/core.js +45 -0
  15. package/dist/hooks/useFeatureTracking.d.ts +34 -0
  16. package/dist/hooks/useFeatureTracking.js +60 -0
  17. package/dist/hooks/useOnboardingTracking.d.ts +85 -0
  18. package/dist/hooks/useOnboardingTracking.js +172 -0
  19. package/dist/hooks/useScreenTracking.d.ts +23 -0
  20. package/dist/hooks/useScreenTracking.js +52 -0
  21. package/dist/hooks/useScrollTracking.d.ts +39 -0
  22. package/dist/hooks/useScrollTracking.js +146 -0
  23. package/dist/index.d.ts +140 -0
  24. package/dist/index.js +171 -0
  25. package/dist/integrations/expoRouter.d.ts +42 -0
  26. package/dist/integrations/expoRouter.js +66 -0
  27. package/dist/integrations/reactNavigation.d.ts +27 -0
  28. package/dist/integrations/reactNavigation.js +101 -0
  29. package/dist/queue.d.ts +52 -0
  30. package/dist/queue.js +143 -0
  31. package/dist/session.d.ts +44 -0
  32. package/dist/session.js +139 -0
  33. package/dist/sessionAnalytics.d.ts +43 -0
  34. package/dist/sessionAnalytics.js +74 -0
  35. package/dist/storage.d.ts +67 -0
  36. package/dist/storage.js +210 -0
  37. package/dist/transport.d.ts +41 -0
  38. package/dist/transport.js +158 -0
  39. package/dist/types.d.ts +102 -0
  40. package/dist/types.js +5 -0
  41. package/dist/utils.d.ts +39 -0
  42. package/dist/utils.js +116 -0
  43. package/package.json +44 -0
@@ -0,0 +1,210 @@
1
+ "use strict";
2
+ /**
3
+ * Storage layer using AsyncStorage for persistence
4
+ * Handles event queue, user IDs, session data, and preferences
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.Storage = void 0;
11
+ const async_storage_1 = __importDefault(require("@react-native-async-storage/async-storage"));
12
+ const constants_1 = require("./constants");
13
+ const utils_1 = require("./utils");
14
+ class Storage {
15
+ /**
16
+ * Get all queued events
17
+ */
18
+ static async getQueuedEvents() {
19
+ try {
20
+ const data = await async_storage_1.default.getItem(constants_1.STORAGE_KEYS.EVENTS_QUEUE);
21
+ if (!data)
22
+ return [];
23
+ return JSON.parse(data);
24
+ }
25
+ catch (error) {
26
+ console.error('[Axyle] Failed to get queued events:', error);
27
+ return [];
28
+ }
29
+ }
30
+ /**
31
+ * Save events to queue
32
+ */
33
+ static async saveQueuedEvents(events) {
34
+ try {
35
+ await async_storage_1.default.setItem(constants_1.STORAGE_KEYS.EVENTS_QUEUE, (0, utils_1.safeStringify)(events));
36
+ }
37
+ catch (error) {
38
+ console.error('[Axyle] Failed to save queued events:', error);
39
+ }
40
+ }
41
+ /**
42
+ * Add event to queue
43
+ */
44
+ static async addEventToQueue(event) {
45
+ try {
46
+ const events = await this.getQueuedEvents();
47
+ events.push(event);
48
+ await this.saveQueuedEvents(events);
49
+ }
50
+ catch (error) {
51
+ console.error('[Axyle] Failed to add event to queue:', error);
52
+ }
53
+ }
54
+ /**
55
+ * Remove events from queue
56
+ */
57
+ static async removeEventsFromQueue(eventIds) {
58
+ try {
59
+ const events = await this.getQueuedEvents();
60
+ const filtered = events.filter((e) => !eventIds.includes(e.id));
61
+ await this.saveQueuedEvents(filtered);
62
+ }
63
+ catch (error) {
64
+ console.error('[Axyle] Failed to remove events from queue:', error);
65
+ }
66
+ }
67
+ /**
68
+ * Clear all queued events
69
+ */
70
+ static async clearQueue() {
71
+ try {
72
+ await async_storage_1.default.removeItem(constants_1.STORAGE_KEYS.EVENTS_QUEUE);
73
+ }
74
+ catch (error) {
75
+ console.error('[Axyle] Failed to clear queue:', error);
76
+ }
77
+ }
78
+ /**
79
+ * Get anonymous ID (create if doesn't exist)
80
+ */
81
+ static async getAnonymousId() {
82
+ try {
83
+ let anonymousId = await async_storage_1.default.getItem(constants_1.STORAGE_KEYS.ANONYMOUS_ID);
84
+ if (!anonymousId) {
85
+ // Generate new anonymous ID
86
+ anonymousId = `anon_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
87
+ await async_storage_1.default.setItem(constants_1.STORAGE_KEYS.ANONYMOUS_ID, anonymousId);
88
+ }
89
+ return anonymousId;
90
+ }
91
+ catch (error) {
92
+ console.error('[Axyle] Failed to get anonymous ID:', error);
93
+ // Fallback to in-memory ID
94
+ return `anon_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
95
+ }
96
+ }
97
+ /**
98
+ * Get user ID
99
+ */
100
+ static async getUserId() {
101
+ try {
102
+ return await async_storage_1.default.getItem(constants_1.STORAGE_KEYS.USER_ID);
103
+ }
104
+ catch (error) {
105
+ console.error('[Axyle] Failed to get user ID:', error);
106
+ return null;
107
+ }
108
+ }
109
+ /**
110
+ * Set user ID
111
+ */
112
+ static async setUserId(userId) {
113
+ try {
114
+ await async_storage_1.default.setItem(constants_1.STORAGE_KEYS.USER_ID, userId);
115
+ }
116
+ catch (error) {
117
+ console.error('[Axyle] Failed to set user ID:', error);
118
+ }
119
+ }
120
+ /**
121
+ * Clear user ID (on reset/logout)
122
+ */
123
+ static async clearUserId() {
124
+ try {
125
+ await async_storage_1.default.removeItem(constants_1.STORAGE_KEYS.USER_ID);
126
+ }
127
+ catch (error) {
128
+ console.error('[Axyle] Failed to clear user ID:', error);
129
+ }
130
+ }
131
+ /**
132
+ * Get session data
133
+ */
134
+ static async getSessionData() {
135
+ try {
136
+ const data = await async_storage_1.default.getItem(constants_1.STORAGE_KEYS.SESSION_DATA);
137
+ if (!data)
138
+ return null;
139
+ return JSON.parse(data);
140
+ }
141
+ catch (error) {
142
+ console.error('[Axyle] Failed to get session data:', error);
143
+ return null;
144
+ }
145
+ }
146
+ /**
147
+ * Save session data
148
+ */
149
+ static async saveSessionData(session) {
150
+ try {
151
+ await async_storage_1.default.setItem(constants_1.STORAGE_KEYS.SESSION_DATA, (0, utils_1.safeStringify)(session));
152
+ }
153
+ catch (error) {
154
+ console.error('[Axyle] Failed to save session data:', error);
155
+ }
156
+ }
157
+ /**
158
+ * Clear session data
159
+ */
160
+ static async clearSessionData() {
161
+ try {
162
+ await async_storage_1.default.removeItem(constants_1.STORAGE_KEYS.SESSION_DATA);
163
+ }
164
+ catch (error) {
165
+ console.error('[Axyle] Failed to clear session data:', error);
166
+ }
167
+ }
168
+ /**
169
+ * Check if user has opted out
170
+ */
171
+ static async isOptedOut() {
172
+ try {
173
+ const value = await async_storage_1.default.getItem(constants_1.STORAGE_KEYS.OPT_OUT);
174
+ return value === 'true';
175
+ }
176
+ catch (error) {
177
+ console.error('[Axyle] Failed to check opt-out status:', error);
178
+ return false;
179
+ }
180
+ }
181
+ /**
182
+ * Set opt-out status
183
+ */
184
+ static async setOptOut(optOut) {
185
+ try {
186
+ await async_storage_1.default.setItem(constants_1.STORAGE_KEYS.OPT_OUT, optOut ? 'true' : 'false');
187
+ }
188
+ catch (error) {
189
+ console.error('[Axyle] Failed to set opt-out status:', error);
190
+ }
191
+ }
192
+ /**
193
+ * Clear all analytics data
194
+ */
195
+ static async clearAll() {
196
+ try {
197
+ await async_storage_1.default.multiRemove([
198
+ constants_1.STORAGE_KEYS.EVENTS_QUEUE,
199
+ constants_1.STORAGE_KEYS.USER_ID,
200
+ constants_1.STORAGE_KEYS.SESSION_DATA,
201
+ constants_1.STORAGE_KEYS.OPT_OUT,
202
+ // Keep anonymous ID for continuity
203
+ ]);
204
+ }
205
+ catch (error) {
206
+ console.error('[Axyle] Failed to clear all data:', error);
207
+ }
208
+ }
209
+ }
210
+ exports.Storage = Storage;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Transport layer for sending events to the analytics API
3
+ * Handles batching, retry logic with exponential backoff
4
+ */
5
+ import { AnalyticsEvent } from "./types";
6
+ export interface TransportConfig {
7
+ apiKey: string;
8
+ baseUrl: string;
9
+ debug: boolean;
10
+ }
11
+ export declare class Transport {
12
+ private config;
13
+ private logger;
14
+ constructor(config: TransportConfig, logger: any);
15
+ /**
16
+ * Send a batch of events to the API
17
+ * Returns the IDs of successfully sent events
18
+ */
19
+ sendBatch(events: AnalyticsEvent[]): Promise<string[]>;
20
+ /**
21
+ * Send a single batch with retry logic
22
+ */
23
+ private sendBatchWithRetry;
24
+ /**
25
+ * Make HTTP request to the API
26
+ */
27
+ private makeRequest;
28
+ /**
29
+ * Split events into batches based on size and count limits
30
+ */
31
+ private createBatches;
32
+ /**
33
+ * Update API key
34
+ */
35
+ updateApiKey(apiKey: string): void;
36
+ /**
37
+ * Update base URL - disabled, always uses production server
38
+ * This method is kept for backward compatibility but does nothing
39
+ */
40
+ updateBaseUrl(_baseUrl: string): void;
41
+ }
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ /**
3
+ * Transport layer for sending events to the analytics API
4
+ * Handles batching, retry logic with exponential backoff
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.Transport = void 0;
8
+ const constants_1 = require("./constants");
9
+ const utils_1 = require("./utils");
10
+ class Transport {
11
+ constructor(config, logger) {
12
+ this.config = config;
13
+ this.logger = logger;
14
+ }
15
+ /**
16
+ * Send a batch of events to the API
17
+ * Returns the IDs of successfully sent events
18
+ */
19
+ async sendBatch(events) {
20
+ if (events.length === 0)
21
+ return [];
22
+ // Split into smaller batches if needed
23
+ const batches = this.createBatches(events);
24
+ const sentEventIds = [];
25
+ for (const batch of batches) {
26
+ try {
27
+ const success = await this.sendBatchWithRetry(batch);
28
+ if (success) {
29
+ sentEventIds.push(...batch.map((e) => e.id));
30
+ }
31
+ }
32
+ catch (error) {
33
+ this.logger.error("Failed to send batch after retries:", error);
34
+ // Continue with next batch even if this one fails
35
+ }
36
+ }
37
+ return sentEventIds;
38
+ }
39
+ /**
40
+ * Send a single batch with retry logic
41
+ */
42
+ async sendBatchWithRetry(events) {
43
+ let attempt = 0;
44
+ while (attempt < constants_1.RETRY_CONFIG.maxRetries) {
45
+ try {
46
+ const response = await this.makeRequest(events);
47
+ if (response.ok) {
48
+ this.logger.log(`Successfully sent ${events.length} events`);
49
+ return true;
50
+ }
51
+ // Get error details for better debugging
52
+ const errorText = await response.text().catch(() => "Unknown error");
53
+ this.logger.error(`API error ${response.status}:`, errorText);
54
+ // Check if error is retryable
55
+ if (response.status >= 400 && response.status < 500) {
56
+ // Client error - don't retry
57
+ this.logger.error(`Client error ${response.status}, not retrying`);
58
+ return false;
59
+ }
60
+ // Server error - retry with backoff
61
+ this.logger.warn(`Server error ${response.status}, retrying...`);
62
+ }
63
+ catch (error) {
64
+ this.logger.warn(`Network error on attempt ${attempt + 1}:`, error);
65
+ if (error instanceof Error) {
66
+ this.logger.warn(`Error details: ${error.message}`);
67
+ }
68
+ }
69
+ // Calculate backoff delay
70
+ if (attempt < constants_1.RETRY_CONFIG.maxRetries - 1) {
71
+ const delay = (0, utils_1.calculateBackoff)(attempt, constants_1.RETRY_CONFIG.initialDelay, constants_1.RETRY_CONFIG.maxDelay, constants_1.RETRY_CONFIG.backoffMultiplier);
72
+ this.logger.log(`Waiting ${delay}ms before retry...`);
73
+ await (0, utils_1.sleep)(delay);
74
+ }
75
+ attempt++;
76
+ }
77
+ return false;
78
+ }
79
+ /**
80
+ * Make HTTP request to the API
81
+ */
82
+ async makeRequest(events) {
83
+ const url = `${this.config.baseUrl}/api/events`;
84
+ this.logger.log(`Sending ${events.length} events to ${url}`);
85
+ try {
86
+ const requestBody = {
87
+ events,
88
+ sentAt: new Date().toISOString(),
89
+ };
90
+ const response = await fetch(url, {
91
+ method: "POST",
92
+ headers: {
93
+ "Content-Type": "application/json",
94
+ "X-API-Key": this.config.apiKey,
95
+ },
96
+ body: JSON.stringify(requestBody),
97
+ });
98
+ if (!response.ok) {
99
+ const errorText = await response.text().catch(() => "Unknown error");
100
+ this.logger.error(`API request failed: ${response.status} ${response.statusText}`, errorText);
101
+ }
102
+ else {
103
+ this.logger.log(`API request successful: ${response.status}`);
104
+ }
105
+ return response;
106
+ }
107
+ catch (error) {
108
+ this.logger.error("Network error sending events:", error);
109
+ throw error;
110
+ }
111
+ }
112
+ /**
113
+ * Split events into batches based on size and count limits
114
+ */
115
+ createBatches(events) {
116
+ const batches = [];
117
+ let currentBatch = [];
118
+ let currentBatchSize = 0;
119
+ for (const event of events) {
120
+ const eventSize = (0, utils_1.getObjectSize)(event);
121
+ // Check if adding this event would exceed limits
122
+ if (currentBatch.length >= constants_1.MAX_EVENTS_PER_BATCH ||
123
+ currentBatchSize + eventSize > constants_1.MAX_BATCH_SIZE) {
124
+ // Start new batch
125
+ if (currentBatch.length > 0) {
126
+ batches.push(currentBatch);
127
+ }
128
+ currentBatch = [event];
129
+ currentBatchSize = eventSize;
130
+ }
131
+ else {
132
+ // Add to current batch
133
+ currentBatch.push(event);
134
+ currentBatchSize += eventSize;
135
+ }
136
+ }
137
+ // Add final batch
138
+ if (currentBatch.length > 0) {
139
+ batches.push(currentBatch);
140
+ }
141
+ return batches;
142
+ }
143
+ /**
144
+ * Update API key
145
+ */
146
+ updateApiKey(apiKey) {
147
+ this.config.apiKey = apiKey;
148
+ }
149
+ /**
150
+ * Update base URL - disabled, always uses production server
151
+ * This method is kept for backward compatibility but does nothing
152
+ */
153
+ updateBaseUrl(_baseUrl) {
154
+ // baseUrl is not configurable - always uses production server
155
+ // This method is kept for backward compatibility but does nothing
156
+ }
157
+ }
158
+ exports.Transport = Transport;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Core type definitions for Axyle Analytics SDK
3
+ */
4
+ /**
5
+ * SDK initialization accepts only the API key
6
+ * All other settings use internal defaults
7
+ */
8
+ export type AxyleInitConfig = string | {
9
+ apiKey: string;
10
+ };
11
+ /**
12
+ * Internal configuration type with all settings
13
+ * Only apiKey comes from user, everything else uses defaults
14
+ */
15
+ export interface InternalAxyleConfig {
16
+ apiKey: string;
17
+ userId: string;
18
+ environment: "dev" | "prod";
19
+ debug: boolean;
20
+ maxQueueSize: number;
21
+ flushInterval: number;
22
+ sessionTimeout: number;
23
+ baseUrl: string;
24
+ }
25
+ export interface EventProperties {
26
+ [key: string]: string | number | boolean | null | undefined;
27
+ }
28
+ export interface UserTraits {
29
+ [key: string]: string | number | boolean | null | undefined;
30
+ }
31
+ export interface AnalyticsEvent {
32
+ /** Unique event ID */
33
+ id: string;
34
+ /** Event name */
35
+ name: string;
36
+ /** Event properties */
37
+ properties: EventProperties;
38
+ /** Timestamp when event occurred */
39
+ timestamp: number;
40
+ /** User ID (anonymous or identified) */
41
+ userId: string;
42
+ /** Anonymous user ID (persists across sessions) */
43
+ anonymousId: string;
44
+ /** Session ID */
45
+ sessionId: string;
46
+ /** Device metadata */
47
+ context: EventContext;
48
+ /** Schema version */
49
+ schemaVersion: string;
50
+ }
51
+ export interface EventContext {
52
+ /** App information */
53
+ app: {
54
+ name: string;
55
+ version: string;
56
+ build: string;
57
+ namespace: string;
58
+ };
59
+ /** Device information */
60
+ device: {
61
+ id: string | null;
62
+ manufacturer: string | null;
63
+ model: string | null;
64
+ name: string | null;
65
+ type: "PHONE" | "TABLET" | "DESKTOP" | "TV" | "UNKNOWN";
66
+ brand: string | null;
67
+ };
68
+ /** OS information */
69
+ os: {
70
+ name: string;
71
+ version: string;
72
+ };
73
+ /** Screen information */
74
+ screen: {
75
+ width: number;
76
+ height: number;
77
+ density: number;
78
+ };
79
+ /** Locale */
80
+ locale: string;
81
+ /** Timezone */
82
+ timezone: string;
83
+ /** Network information (if available) */
84
+ network?: {
85
+ carrier: string | null;
86
+ wifi: boolean;
87
+ };
88
+ /** Environment */
89
+ environment: "dev" | "prod";
90
+ }
91
+ export interface SessionData {
92
+ sessionId: string;
93
+ startTime: number;
94
+ lastActivityTime: number;
95
+ }
96
+ export interface StorageKeys {
97
+ EVENTS_QUEUE: string;
98
+ ANONYMOUS_ID: string;
99
+ USER_ID: string;
100
+ SESSION_DATA: string;
101
+ OPT_OUT: string;
102
+ }
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ /**
3
+ * Core type definitions for Axyle Analytics SDK
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Utility functions for the SDK
3
+ */
4
+ /**
5
+ * Generate a UUID v4
6
+ */
7
+ export declare function generateUUID(): string;
8
+ /**
9
+ * Safely stringify an object, handling circular references
10
+ */
11
+ export declare function safeStringify(obj: any): string;
12
+ /**
13
+ * Calculate size of an object in bytes
14
+ */
15
+ export declare function getObjectSize(obj: any): number;
16
+ /**
17
+ * Validate event name
18
+ */
19
+ export declare function isValidEventName(name: string): boolean;
20
+ /**
21
+ * Sanitize event properties
22
+ */
23
+ export declare function sanitizeProperties(properties: Record<string, any>): Record<string, any>;
24
+ /**
25
+ * Create a logger that respects debug mode
26
+ */
27
+ export declare function createLogger(debug: boolean): {
28
+ log: (...args: any[]) => void;
29
+ warn: (...args: any[]) => void;
30
+ error: (...args: any[]) => void;
31
+ };
32
+ /**
33
+ * Sleep for a given number of milliseconds
34
+ */
35
+ export declare function sleep(ms: number): Promise<void>;
36
+ /**
37
+ * Calculate exponential backoff delay
38
+ */
39
+ export declare function calculateBackoff(attempt: number, initialDelay: number, maxDelay: number, multiplier: number): number;
package/dist/utils.js ADDED
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ /**
3
+ * Utility functions for the SDK
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateUUID = generateUUID;
7
+ exports.safeStringify = safeStringify;
8
+ exports.getObjectSize = getObjectSize;
9
+ exports.isValidEventName = isValidEventName;
10
+ exports.sanitizeProperties = sanitizeProperties;
11
+ exports.createLogger = createLogger;
12
+ exports.sleep = sleep;
13
+ exports.calculateBackoff = calculateBackoff;
14
+ /**
15
+ * Generate a UUID v4
16
+ */
17
+ function generateUUID() {
18
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
19
+ const r = (Math.random() * 16) | 0;
20
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
21
+ return v.toString(16);
22
+ });
23
+ }
24
+ /**
25
+ * Safely stringify an object, handling circular references
26
+ */
27
+ function safeStringify(obj) {
28
+ const seen = new WeakSet();
29
+ return JSON.stringify(obj, (key, value) => {
30
+ if (typeof value === 'object' && value !== null) {
31
+ if (seen.has(value)) {
32
+ return '[Circular]';
33
+ }
34
+ seen.add(value);
35
+ }
36
+ return value;
37
+ });
38
+ }
39
+ /**
40
+ * Calculate size of an object in bytes
41
+ */
42
+ function getObjectSize(obj) {
43
+ return new Blob([safeStringify(obj)]).size;
44
+ }
45
+ /**
46
+ * Validate event name
47
+ */
48
+ function isValidEventName(name) {
49
+ if (!name || typeof name !== 'string')
50
+ return false;
51
+ if (name.length === 0 || name.length > 255)
52
+ return false;
53
+ // Allow alphanumeric, spaces, underscores, hyphens
54
+ return /^[a-zA-Z0-9\s_-]+$/.test(name);
55
+ }
56
+ /**
57
+ * Sanitize event properties
58
+ */
59
+ function sanitizeProperties(properties) {
60
+ const sanitized = {};
61
+ for (const [key, value] of Object.entries(properties)) {
62
+ // Skip invalid keys
63
+ if (!key || typeof key !== 'string' || key.length > 255)
64
+ continue;
65
+ // Only allow primitive types
66
+ if (typeof value === 'string' ||
67
+ typeof value === 'number' ||
68
+ typeof value === 'boolean' ||
69
+ value === null ||
70
+ value === undefined) {
71
+ sanitized[key] = value;
72
+ }
73
+ else if (typeof value === 'object') {
74
+ // Convert objects/arrays to JSON strings
75
+ try {
76
+ sanitized[key] = safeStringify(value);
77
+ }
78
+ catch (e) {
79
+ // Skip if can't stringify
80
+ }
81
+ }
82
+ }
83
+ return sanitized;
84
+ }
85
+ /**
86
+ * Create a logger that respects debug mode
87
+ */
88
+ function createLogger(debug) {
89
+ return {
90
+ log: (...args) => {
91
+ if (debug)
92
+ console.log('[Axyle]', ...args);
93
+ },
94
+ warn: (...args) => {
95
+ if (debug)
96
+ console.warn('[Axyle]', ...args);
97
+ },
98
+ error: (...args) => {
99
+ if (debug)
100
+ console.error('[Axyle]', ...args);
101
+ },
102
+ };
103
+ }
104
+ /**
105
+ * Sleep for a given number of milliseconds
106
+ */
107
+ function sleep(ms) {
108
+ return new Promise((resolve) => setTimeout(resolve, ms));
109
+ }
110
+ /**
111
+ * Calculate exponential backoff delay
112
+ */
113
+ function calculateBackoff(attempt, initialDelay, maxDelay, multiplier) {
114
+ const delay = initialDelay * Math.pow(multiplier, attempt);
115
+ return Math.min(delay, maxDelay);
116
+ }