@jayethian/axiom 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -1 +1,150 @@
1
- # axiom
1
+ <div align="center">
2
+ <img src="./assets/logo.png" alt="axiom logo" width="400" />
3
+ <h1>Axiom</h1>
4
+
5
+ <h3>Resilient, offline-first networking for modern React apps.</h3>
6
+
7
+ <p>
8
+ <a href="https://www.npmjs.com/package/@jayethian/axiom">
9
+ <img src="https://img.shields.io/npm/v/@jayethian/axiom.svg?style=flat-square" alt="npm version" />
10
+ </a>
11
+ <a href="https://www.npmjs.com/package/@jayethian/axiom">
12
+ <img src="https://img.shields.io/npm/dm/@jayethian/axiom.svg?style=flat-square" alt="npm downloads" />
13
+ </a>
14
+ <a href="https://opensource.org/licenses/MIT">
15
+ <img src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" alt="License: MIT" />
16
+ </a>
17
+ <img src="https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg?style=flat-square" alt="TypeScript" />
18
+ </p>
19
+ </div>
20
+
21
+ <br />
22
+
23
+ ## The Problem
24
+ Standard HTTP clients like **Axios** or **Fetch** assume a stable connection. When a user submits data in a dead zone (elevators, basements, rural areas), the request simply fails. Without a complex manual retry system, **that data is gone forever.**
25
+
26
+ ## The Axiom Way
27
+ Axiom intercepts network failures and timeouts. Instead of throwing an error, it serializes the request and moves it to a persistent local queue. When the connection returns, Axiom flushes the queue automatically.
28
+
29
+ ```typescript
30
+ // Standard Fetch: Fails and loses data when offline.
31
+ await fetch('/api/orders', { method: 'POST', body: data });
32
+
33
+ // Axiom: Intercepts drop, queues safely, and syncs when back online.
34
+ await axiom.post('/api/orders', data);
35
+
36
+ ```
37
+
38
+ ---
39
+
40
+ ## Key Features
41
+
42
+ * **📱 Mobile-First Resilience:** Specifically tuned for React Native's shaky connectivity.
43
+ * **🔄 Autonomous Background Sync:** Replays the queue the moment a signal is detected.
44
+ * **⚡ Priority Lanes:** Ensure critical data (e.g., payments) jumps to the front of the queue ahead of background tasks (e.g., analytics).
45
+ * **🛡️ Just-In-Time Headers:** Refresh Auth Tokens immediately before syncing to prevent `401 Unauthorized` errors on old requests.
46
+ * **💾 Storage Agnostic:** Use the high-performance storage of your choice (**MMKV**, **SQLite**, **IndexedDB**).
47
+ * **🪦 Dead Letter Queues:** Protects your app from infinite loops by isolating permanently failing requests.
48
+
49
+ ---
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ yarn add @jayethian/axiom
55
+ # or
56
+ npm install @jayethian/axiom
57
+
58
+ ```
59
+
60
+ ---
61
+
62
+ ## Setup & Usage
63
+
64
+ ### 1. Initialize the Provider
65
+
66
+ Wrap your app root. Axiom doesn't force a specific network library on you—just pass in your preferred listener (like NetInfo).
67
+
68
+ ```tsx
69
+ import { AxiomProvider } from '@jayethian/axiom';
70
+ import NetInfo from '@react-native-community/netinfo';
71
+
72
+ export default function App() {
73
+ return (
74
+ <AxiomProvider
75
+ config={{ baseURL: '[https://api.myapp.com](https://api.myapp.com)', timeout: 10000 }}
76
+ networkListener={(callback) => {
77
+ return NetInfo.addEventListener(state => callback(!!state.isConnected));
78
+ }}
79
+ >
80
+ <Main />
81
+ </AxiomProvider>
82
+ );
83
+ }
84
+
85
+ ```
86
+
87
+ ### 2. Use Hooks for Better UX
88
+
89
+ Keep your users informed when they are working offline.
90
+
91
+ ```tsx
92
+ import { axiom, useAxiomQueue } from '@jayethian/axiom';
93
+
94
+ const { isOnline } = useAxiomQueue();
95
+
96
+ const onSave = async (data) => {
97
+ const res = await axiom.post('/profile', data, { priority: 'urgent' });
98
+
99
+ if (res.isQueued) {
100
+ showToast("Working offline. Your changes will sync automatically!");
101
+ }
102
+ };
103
+
104
+ ```
105
+
106
+ ---
107
+
108
+ ## Advanced Configuration
109
+
110
+ ### Priority Lanes
111
+
112
+ Prevent large background uploads from blocking small, critical API calls.
113
+
114
+ ```typescript
115
+ // This stays at the back of the line
116
+ axiom.post('/analytics', logData, { priority: 'background' });
117
+
118
+ // This jumps to the front
119
+ axiom.post('/chat/send', message, { priority: 'urgent' });
120
+
121
+ ```
122
+
123
+ ### Just-In-Time Headers
124
+
125
+ Refresh your JWT right before the queue fires to ensure every request is authorized.
126
+
127
+ ```tsx
128
+ <AxiomProvider
129
+ config={{
130
+ onBeforeSync: async (request) => {
131
+ const token = await getFreshToken();
132
+ return {
133
+ ...request,
134
+ headers: { ...request.headers, Authorization: `Bearer ${token}` }
135
+ };
136
+ }
137
+ }}
138
+ >
139
+
140
+ ```
141
+
142
+ ---
143
+
144
+ ## Contributing
145
+
146
+ We welcome contributions! Please feel free to submit a [Pull Request](https://www.google.com/search?q=https://github.com/jayethian/axiom/pulls) or report a [Bug](https://www.google.com/search?q=https://github.com/jayethian/axiom/issues).
147
+
148
+ ## License
149
+
150
+ Distributed under the MIT License. See `LICENSE` for more information. Built with ⚡ by [Jayetheus](https://www.google.com/search?q=https://github.com/jayethian).
Binary file
package/dist/index.d.mts CHANGED
@@ -1,6 +1,11 @@
1
1
  import React from 'react';
2
2
 
3
+ /** * Defines how the request should be treated when the queue flushes.
4
+ * Urgent requests bypass the background queue entirely if the network is active.
5
+ */
3
6
  type RequestPriority = 'urgent' | 'background';
7
+ /** * Represents a serialized HTTP request frozen in offline storage.
8
+ */
4
9
  interface QueuedRequest {
5
10
  id: string;
6
11
  timestamp: number;
@@ -11,13 +16,32 @@ interface QueuedRequest {
11
16
  priority: RequestPriority;
12
17
  retryCount: number;
13
18
  }
19
+ /** * Global configuration for the Axiom engine initialized on startup.
20
+ */
14
21
  interface AxiomConfig {
22
+ /** The base URL prepended to all request paths. */
15
23
  baseURL?: string;
24
+ /** Global headers applied to every request (e.g., Auth tokens). */
16
25
  defaultHeaders?: Record<string, string>;
26
+ /** The maximum number of times a queued request will attempt to sync before failing permanently. */
17
27
  maxRetries?: number;
28
+ /** Global timeout in milliseconds before a request is aborted and queued. */
29
+ timeout?: number;
30
+ /** Middleware hook triggered immediately before a queued request is synced. Ideal for refreshing Auth tokens. */
18
31
  onBeforeSync?: (request: QueuedRequest) => Promise<QueuedRequest>;
32
+ /** Callback triggered when a request exceeds maxRetries and is permanently removed from the queue. */
19
33
  onDeadLetter?: (request: QueuedRequest, error: Error) => void;
20
34
  }
35
+ /** * Per-request configuration options that override the global configuration.
36
+ */
37
+ interface AxiomRequestOptions {
38
+ /** Overrides the queue sorting behavior for this specific request. */
39
+ priority?: RequestPriority;
40
+ /** Overrides the global timeout limit for this specific request. */
41
+ timeout?: number;
42
+ /** Specific headers to append to this single request (e.g., custom Content-Type). */
43
+ headers?: Record<string, string>;
44
+ }
21
45
 
22
46
  interface AxiomStorageAdapter {
23
47
  /** Saves a request to the persistent queue */
@@ -43,40 +67,88 @@ declare class AxiomEngine {
43
67
  private storage;
44
68
  private syncManager;
45
69
  /**
46
- * Initializes the Axiom engine with your specific rules and storage.
70
+ * Initializes the Axiom engine with global configuration and a storage adapter.
71
+ * This must be called before making any requests to enable persistence.
72
+ * * @param config - Global configuration (baseURL, timeouts, custom headers, etc.)
73
+ * @param storageAdapter - Optional custom adapter (e.g., MMKV). Defaults to in-memory storage.
47
74
  */
48
75
  create(config: AxiomConfig, storageAdapter?: AxiomStorageAdapter): void;
76
+ /**
77
+ * Manually triggers the background sync manager to flush all pending queued requests.
78
+ * Note: This is automatically handled by `AxiomProvider` when the network reconnects.
79
+ */
49
80
  forceSync(): Promise<void>;
50
81
  /**
51
- * Generates a unique ID for queued requests.
82
+ * Generates a unique collision-resistant ID for queued requests.
52
83
  */
53
84
  private generateId;
54
85
  /**
55
- * The core POST method.
86
+ * Executes an HTTP GET request.
87
+ * If the network is unavailable or times out, the request is safely queued.
88
+ * * @param url - The endpoint URL (appended to baseURL if configured).
89
+ * @param options - Request-specific options (priority lanes, timeout overrides).
90
+ * @returns A promise resolving to the response data, status code, and queue state.
91
+ */
92
+ get<T>(url: string, options?: AxiomRequestOptions): Promise<{
93
+ data?: T;
94
+ status: number;
95
+ isQueued: boolean;
96
+ }>;
97
+ /**
98
+ * Executes an HTTP POST request.
99
+ * If the network is unavailable or times out, the payload is safely queued.
100
+ * * @param url - The endpoint URL.
101
+ * @param data - The payload object to be serialized and sent.
102
+ * @param options - Request-specific options.
103
+ */
104
+ post<T>(url: string, data?: any, options?: AxiomRequestOptions): Promise<{
105
+ data?: T;
106
+ status: number;
107
+ isQueued: boolean;
108
+ }>;
109
+ /**
110
+ * Executes an HTTP PUT request to entirely replace a resource.
111
+ * * @param url - The endpoint URL.
112
+ * @param data - The payload object to be serialized and sent.
113
+ * @param options - Request-specific options.
114
+ */
115
+ put<T>(url: string, data?: any, options?: AxiomRequestOptions): Promise<{
116
+ data?: T;
117
+ status: number;
118
+ isQueued: boolean;
119
+ }>;
120
+ /**
121
+ * Executes an HTTP PATCH request to partially update a resource.
122
+ * * @param url - The endpoint URL.
123
+ * @param data - The partial payload object to be serialized and sent.
124
+ * @param options - Request-specific options.
56
125
  */
57
- post<T>(url: string, data?: any, options?: {
58
- priority?: RequestPriority;
59
- }): Promise<{
126
+ patch<T>(url: string, data?: any, options?: AxiomRequestOptions): Promise<{
60
127
  data?: T;
61
128
  status: number;
62
129
  isQueued: boolean;
63
130
  }>;
64
- /** The core GET method.
65
- * Note: GET requests typically don't have a body, but we still want to queue them if offline.
131
+ /**
132
+ * Executes an HTTP DELETE request.
133
+ * * @param url - The endpoint URL.
134
+ * @param options - Request-specific options.
66
135
  */
67
- get<T>(url: string, options?: {
68
- priority?: RequestPriority;
69
- }): Promise<{
136
+ delete<T>(url: string, options?: AxiomRequestOptions): Promise<{
70
137
  data?: T;
71
138
  status: number;
72
139
  isQueued: boolean;
73
140
  }>;
141
+ /**
142
+ * Internal helper to consolidate request preparation and keep the engine DRY.
143
+ */
144
+ private prepareRequest;
74
145
  /**
75
146
  * Internal logic to fire the request or catch the network drop.
147
+ * Handles timeout cancellations via AbortController.
76
148
  */
77
149
  private attemptFetch;
78
150
  /**
79
- * Saves the request to whatever storage adapter was provided on startup.
151
+ * Saves the request to the configured storage adapter.
80
152
  */
81
153
  private enqueueRequest;
82
154
  }
@@ -99,4 +171,4 @@ declare function useAxiomQueue(): {
99
171
  forceSync: () => Promise<void>;
100
172
  };
101
173
 
102
- export { type AxiomConfig, AxiomEngine, AxiomProvider, type AxiomStorageAdapter, MemoryStorageAdapter, type QueuedRequest, type RequestPriority, axiom, useAxiomQueue };
174
+ export { type AxiomConfig, AxiomEngine, AxiomProvider, type AxiomRequestOptions, type AxiomStorageAdapter, MemoryStorageAdapter, type QueuedRequest, type RequestPriority, axiom, useAxiomQueue };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import React from 'react';
2
2
 
3
+ /** * Defines how the request should be treated when the queue flushes.
4
+ * Urgent requests bypass the background queue entirely if the network is active.
5
+ */
3
6
  type RequestPriority = 'urgent' | 'background';
7
+ /** * Represents a serialized HTTP request frozen in offline storage.
8
+ */
4
9
  interface QueuedRequest {
5
10
  id: string;
6
11
  timestamp: number;
@@ -11,13 +16,32 @@ interface QueuedRequest {
11
16
  priority: RequestPriority;
12
17
  retryCount: number;
13
18
  }
19
+ /** * Global configuration for the Axiom engine initialized on startup.
20
+ */
14
21
  interface AxiomConfig {
22
+ /** The base URL prepended to all request paths. */
15
23
  baseURL?: string;
24
+ /** Global headers applied to every request (e.g., Auth tokens). */
16
25
  defaultHeaders?: Record<string, string>;
26
+ /** The maximum number of times a queued request will attempt to sync before failing permanently. */
17
27
  maxRetries?: number;
28
+ /** Global timeout in milliseconds before a request is aborted and queued. */
29
+ timeout?: number;
30
+ /** Middleware hook triggered immediately before a queued request is synced. Ideal for refreshing Auth tokens. */
18
31
  onBeforeSync?: (request: QueuedRequest) => Promise<QueuedRequest>;
32
+ /** Callback triggered when a request exceeds maxRetries and is permanently removed from the queue. */
19
33
  onDeadLetter?: (request: QueuedRequest, error: Error) => void;
20
34
  }
35
+ /** * Per-request configuration options that override the global configuration.
36
+ */
37
+ interface AxiomRequestOptions {
38
+ /** Overrides the queue sorting behavior for this specific request. */
39
+ priority?: RequestPriority;
40
+ /** Overrides the global timeout limit for this specific request. */
41
+ timeout?: number;
42
+ /** Specific headers to append to this single request (e.g., custom Content-Type). */
43
+ headers?: Record<string, string>;
44
+ }
21
45
 
22
46
  interface AxiomStorageAdapter {
23
47
  /** Saves a request to the persistent queue */
@@ -43,40 +67,88 @@ declare class AxiomEngine {
43
67
  private storage;
44
68
  private syncManager;
45
69
  /**
46
- * Initializes the Axiom engine with your specific rules and storage.
70
+ * Initializes the Axiom engine with global configuration and a storage adapter.
71
+ * This must be called before making any requests to enable persistence.
72
+ * * @param config - Global configuration (baseURL, timeouts, custom headers, etc.)
73
+ * @param storageAdapter - Optional custom adapter (e.g., MMKV). Defaults to in-memory storage.
47
74
  */
48
75
  create(config: AxiomConfig, storageAdapter?: AxiomStorageAdapter): void;
76
+ /**
77
+ * Manually triggers the background sync manager to flush all pending queued requests.
78
+ * Note: This is automatically handled by `AxiomProvider` when the network reconnects.
79
+ */
49
80
  forceSync(): Promise<void>;
50
81
  /**
51
- * Generates a unique ID for queued requests.
82
+ * Generates a unique collision-resistant ID for queued requests.
52
83
  */
53
84
  private generateId;
54
85
  /**
55
- * The core POST method.
86
+ * Executes an HTTP GET request.
87
+ * If the network is unavailable or times out, the request is safely queued.
88
+ * * @param url - The endpoint URL (appended to baseURL if configured).
89
+ * @param options - Request-specific options (priority lanes, timeout overrides).
90
+ * @returns A promise resolving to the response data, status code, and queue state.
91
+ */
92
+ get<T>(url: string, options?: AxiomRequestOptions): Promise<{
93
+ data?: T;
94
+ status: number;
95
+ isQueued: boolean;
96
+ }>;
97
+ /**
98
+ * Executes an HTTP POST request.
99
+ * If the network is unavailable or times out, the payload is safely queued.
100
+ * * @param url - The endpoint URL.
101
+ * @param data - The payload object to be serialized and sent.
102
+ * @param options - Request-specific options.
103
+ */
104
+ post<T>(url: string, data?: any, options?: AxiomRequestOptions): Promise<{
105
+ data?: T;
106
+ status: number;
107
+ isQueued: boolean;
108
+ }>;
109
+ /**
110
+ * Executes an HTTP PUT request to entirely replace a resource.
111
+ * * @param url - The endpoint URL.
112
+ * @param data - The payload object to be serialized and sent.
113
+ * @param options - Request-specific options.
114
+ */
115
+ put<T>(url: string, data?: any, options?: AxiomRequestOptions): Promise<{
116
+ data?: T;
117
+ status: number;
118
+ isQueued: boolean;
119
+ }>;
120
+ /**
121
+ * Executes an HTTP PATCH request to partially update a resource.
122
+ * * @param url - The endpoint URL.
123
+ * @param data - The partial payload object to be serialized and sent.
124
+ * @param options - Request-specific options.
56
125
  */
57
- post<T>(url: string, data?: any, options?: {
58
- priority?: RequestPriority;
59
- }): Promise<{
126
+ patch<T>(url: string, data?: any, options?: AxiomRequestOptions): Promise<{
60
127
  data?: T;
61
128
  status: number;
62
129
  isQueued: boolean;
63
130
  }>;
64
- /** The core GET method.
65
- * Note: GET requests typically don't have a body, but we still want to queue them if offline.
131
+ /**
132
+ * Executes an HTTP DELETE request.
133
+ * * @param url - The endpoint URL.
134
+ * @param options - Request-specific options.
66
135
  */
67
- get<T>(url: string, options?: {
68
- priority?: RequestPriority;
69
- }): Promise<{
136
+ delete<T>(url: string, options?: AxiomRequestOptions): Promise<{
70
137
  data?: T;
71
138
  status: number;
72
139
  isQueued: boolean;
73
140
  }>;
141
+ /**
142
+ * Internal helper to consolidate request preparation and keep the engine DRY.
143
+ */
144
+ private prepareRequest;
74
145
  /**
75
146
  * Internal logic to fire the request or catch the network drop.
147
+ * Handles timeout cancellations via AbortController.
76
148
  */
77
149
  private attemptFetch;
78
150
  /**
79
- * Saves the request to whatever storage adapter was provided on startup.
151
+ * Saves the request to the configured storage adapter.
80
152
  */
81
153
  private enqueueRequest;
82
154
  }
@@ -99,4 +171,4 @@ declare function useAxiomQueue(): {
99
171
  forceSync: () => Promise<void>;
100
172
  };
101
173
 
102
- export { type AxiomConfig, AxiomEngine, AxiomProvider, type AxiomStorageAdapter, MemoryStorageAdapter, type QueuedRequest, type RequestPriority, axiom, useAxiomQueue };
174
+ export { type AxiomConfig, AxiomEngine, AxiomProvider, type AxiomRequestOptions, type AxiomStorageAdapter, MemoryStorageAdapter, type QueuedRequest, type RequestPriority, axiom, useAxiomQueue };
package/dist/index.js CHANGED
@@ -56,6 +56,7 @@ var SyncManager = class {
56
56
  }
57
57
  /**
58
58
  * The master trigger. Call this when the OS reports network is back online.
59
+ * Automatically sorts requests so 'urgent' items bypass 'background' items.
59
60
  */
60
61
  async flushQueue() {
61
62
  if (this.isSyncing) return;
@@ -63,11 +64,15 @@ var SyncManager = class {
63
64
  try {
64
65
  const pending = await this.storage.getAll();
65
66
  if (pending.length === 0) {
66
- this.isSyncing = false;
67
67
  return;
68
68
  }
69
69
  console.log(`[Axiom] Network restored. Syncing ${pending.length} queued requests...`);
70
- for (const request of pending) {
70
+ const sortedQueue = pending.sort((a, b) => {
71
+ if (a.priority === "urgent" && b.priority !== "urgent") return -1;
72
+ if (a.priority !== "urgent" && b.priority === "urgent") return 1;
73
+ return a.timestamp - b.timestamp;
74
+ });
75
+ for (const request of sortedQueue) {
71
76
  await this.processRequest(request);
72
77
  }
73
78
  } finally {
@@ -83,16 +88,22 @@ var SyncManager = class {
83
88
  try {
84
89
  reqToSync = await this.config.onBeforeSync(request);
85
90
  } catch (error) {
86
- console.error(`[Axiom] onBeforeSync failed for ${request.id}. Skipping.`);
91
+ console.error(`[Axiom] onBeforeSync failed for ${request.id}. Marking as failure.`);
92
+ await this.handleFailure(request);
87
93
  return;
88
94
  }
89
95
  }
96
+ const controller = new AbortController();
97
+ const timeoutMs = this.config.timeout || 1e4;
98
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
90
99
  try {
91
100
  const response = await fetch(reqToSync.url, {
92
101
  method: reqToSync.method,
93
102
  headers: reqToSync.headers,
94
- body: reqToSync.body
103
+ body: reqToSync.body,
104
+ signal: controller.signal
95
105
  });
106
+ clearTimeout(timeoutId);
96
107
  if (response.ok || response.status >= 400 && response.status < 500) {
97
108
  await this.storage.remove(reqToSync.id);
98
109
  console.log(`[Axiom] Request ${reqToSync.id} synced successfully.`);
@@ -100,6 +111,7 @@ var SyncManager = class {
100
111
  await this.handleFailure(reqToSync);
101
112
  }
102
113
  } catch (error) {
114
+ clearTimeout(timeoutId);
103
115
  await this.handleFailure(reqToSync);
104
116
  }
105
117
  }
@@ -108,7 +120,7 @@ var SyncManager = class {
108
120
  */
109
121
  async handleFailure(request) {
110
122
  request.retryCount += 1;
111
- const maxRetries = this.config.maxRetries || 3;
123
+ const maxRetries = this.config.maxRetries ?? 3;
112
124
  if (request.retryCount >= maxRetries) {
113
125
  console.warn(`[Axiom] Request ${request.id} failed ${maxRetries} times. Moving to Dead Letter.`);
114
126
  await this.storage.remove(request.id);
@@ -128,7 +140,10 @@ var AxiomEngine = class {
128
140
  this.storage = new MemoryStorageAdapter();
129
141
  }
130
142
  /**
131
- * Initializes the Axiom engine with your specific rules and storage.
143
+ * Initializes the Axiom engine with global configuration and a storage adapter.
144
+ * This must be called before making any requests to enable persistence.
145
+ * * @param config - Global configuration (baseURL, timeouts, custom headers, etc.)
146
+ * @param storageAdapter - Optional custom adapter (e.g., MMKV). Defaults to in-memory storage.
132
147
  */
133
148
  create(config, storageAdapter) {
134
149
  this.config = config;
@@ -137,6 +152,10 @@ var AxiomEngine = class {
137
152
  }
138
153
  this.syncManager = new SyncManager(this.storage, this.config);
139
154
  }
155
+ /**
156
+ * Manually triggers the background sync manager to flush all pending queued requests.
157
+ * Note: This is automatically handled by `AxiomProvider` when the network reconnects.
158
+ */
140
159
  async forceSync() {
141
160
  if (!this.syncManager) {
142
161
  console.error("[Axiom] Engine not initialized. Call axiom.create() first.");
@@ -145,59 +164,94 @@ var AxiomEngine = class {
145
164
  await this.syncManager.flushQueue();
146
165
  }
147
166
  /**
148
- * Generates a unique ID for queued requests.
167
+ * Generates a unique collision-resistant ID for queued requests.
149
168
  */
150
169
  generateId() {
151
170
  return Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
152
171
  }
153
172
  /**
154
- * The core POST method.
173
+ * Executes an HTTP GET request.
174
+ * If the network is unavailable or times out, the request is safely queued.
175
+ * * @param url - The endpoint URL (appended to baseURL if configured).
176
+ * @param options - Request-specific options (priority lanes, timeout overrides).
177
+ * @returns A promise resolving to the response data, status code, and queue state.
178
+ */
179
+ async get(url, options) {
180
+ return this.prepareRequest("GET", url, void 0, options);
181
+ }
182
+ /**
183
+ * Executes an HTTP POST request.
184
+ * If the network is unavailable or times out, the payload is safely queued.
185
+ * * @param url - The endpoint URL.
186
+ * @param data - The payload object to be serialized and sent.
187
+ * @param options - Request-specific options.
155
188
  */
156
189
  async post(url, data, options) {
157
- const fullUrl = this.config.baseURL ? `${this.config.baseURL}${url}` : url;
158
- const request = {
159
- id: this.generateId(),
160
- timestamp: Date.now(),
161
- url: fullUrl,
162
- method: "POST",
163
- headers: {
164
- "Content-Type": "application/json",
165
- ...this.config.defaultHeaders || {}
166
- },
167
- body: data ? JSON.stringify(data) : void 0,
168
- priority: options?.priority || "urgent",
169
- retryCount: 0
170
- };
171
- return this.attemptFetch(request);
190
+ return this.prepareRequest("POST", url, data, options);
172
191
  }
173
- /** The core GET method.
174
- * Note: GET requests typically don't have a body, but we still want to queue them if offline.
192
+ /**
193
+ * Executes an HTTP PUT request to entirely replace a resource.
194
+ * * @param url - The endpoint URL.
195
+ * @param data - The payload object to be serialized and sent.
196
+ * @param options - Request-specific options.
175
197
  */
176
- async get(url, options) {
198
+ async put(url, data, options) {
199
+ return this.prepareRequest("PUT", url, data, options);
200
+ }
201
+ /**
202
+ * Executes an HTTP PATCH request to partially update a resource.
203
+ * * @param url - The endpoint URL.
204
+ * @param data - The partial payload object to be serialized and sent.
205
+ * @param options - Request-specific options.
206
+ */
207
+ async patch(url, data, options) {
208
+ return this.prepareRequest("PATCH", url, data, options);
209
+ }
210
+ /**
211
+ * Executes an HTTP DELETE request.
212
+ * * @param url - The endpoint URL.
213
+ * @param options - Request-specific options.
214
+ */
215
+ async delete(url, options) {
216
+ return this.prepareRequest("DELETE", url, void 0, options);
217
+ }
218
+ /**
219
+ * Internal helper to consolidate request preparation and keep the engine DRY.
220
+ */
221
+ async prepareRequest(method, url, data, options) {
177
222
  const fullUrl = this.config.baseURL ? `${this.config.baseURL}${url}` : url;
223
+ const headers = { ...this.config.defaultHeaders || {} };
224
+ if (options?.headers) {
225
+ Object.assign(headers, options.headers);
226
+ }
178
227
  const request = {
179
228
  id: this.generateId(),
180
229
  timestamp: Date.now(),
181
230
  url: fullUrl,
182
- method: "GET",
183
- headers: {
184
- ...this.config.defaultHeaders || {}
185
- },
231
+ method,
232
+ headers,
233
+ body: data ? JSON.stringify(data) : void 0,
186
234
  priority: options?.priority || "urgent",
187
235
  retryCount: 0
188
236
  };
189
- return this.attemptFetch(request);
237
+ const timeoutMs = options?.timeout || this.config.timeout || 8e3;
238
+ return this.attemptFetch(request, timeoutMs);
190
239
  }
191
240
  /**
192
241
  * Internal logic to fire the request or catch the network drop.
242
+ * Handles timeout cancellations via AbortController.
193
243
  */
194
- async attemptFetch(request) {
244
+ async attemptFetch(request, timeoutMs) {
245
+ const controller = new AbortController();
246
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
195
247
  try {
196
248
  const response = await fetch(request.url, {
197
249
  method: request.method,
198
250
  headers: request.headers,
199
- body: request.body
251
+ body: request.body,
252
+ signal: controller.signal
200
253
  });
254
+ clearTimeout(timeoutId);
201
255
  if (response.ok) {
202
256
  const responseData = await response.json().catch(() => null);
203
257
  return { data: responseData, status: response.status, isQueued: false };
@@ -207,12 +261,16 @@ var AxiomEngine = class {
207
261
  }
208
262
  return { status: response.status, isQueued: false };
209
263
  } catch (error) {
264
+ clearTimeout(timeoutId);
265
+ if (error.name === "AbortError") {
266
+ console.warn(`[Axiom] Request to ${request.url} timed out after ${timeoutMs}ms. Queuing for retry.`);
267
+ }
210
268
  await this.enqueueRequest(request);
211
269
  return { status: 202, isQueued: true };
212
270
  }
213
271
  }
214
272
  /**
215
- * Saves the request to whatever storage adapter was provided on startup.
273
+ * Saves the request to the configured storage adapter.
216
274
  */
217
275
  async enqueueRequest(request) {
218
276
  console.warn(`[Axiom] Network unreachable. Queuing request ${request.id}`);
package/dist/index.mjs CHANGED
@@ -26,6 +26,7 @@ var SyncManager = class {
26
26
  }
27
27
  /**
28
28
  * The master trigger. Call this when the OS reports network is back online.
29
+ * Automatically sorts requests so 'urgent' items bypass 'background' items.
29
30
  */
30
31
  async flushQueue() {
31
32
  if (this.isSyncing) return;
@@ -33,11 +34,15 @@ var SyncManager = class {
33
34
  try {
34
35
  const pending = await this.storage.getAll();
35
36
  if (pending.length === 0) {
36
- this.isSyncing = false;
37
37
  return;
38
38
  }
39
39
  console.log(`[Axiom] Network restored. Syncing ${pending.length} queued requests...`);
40
- for (const request of pending) {
40
+ const sortedQueue = pending.sort((a, b) => {
41
+ if (a.priority === "urgent" && b.priority !== "urgent") return -1;
42
+ if (a.priority !== "urgent" && b.priority === "urgent") return 1;
43
+ return a.timestamp - b.timestamp;
44
+ });
45
+ for (const request of sortedQueue) {
41
46
  await this.processRequest(request);
42
47
  }
43
48
  } finally {
@@ -53,16 +58,22 @@ var SyncManager = class {
53
58
  try {
54
59
  reqToSync = await this.config.onBeforeSync(request);
55
60
  } catch (error) {
56
- console.error(`[Axiom] onBeforeSync failed for ${request.id}. Skipping.`);
61
+ console.error(`[Axiom] onBeforeSync failed for ${request.id}. Marking as failure.`);
62
+ await this.handleFailure(request);
57
63
  return;
58
64
  }
59
65
  }
66
+ const controller = new AbortController();
67
+ const timeoutMs = this.config.timeout || 1e4;
68
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
60
69
  try {
61
70
  const response = await fetch(reqToSync.url, {
62
71
  method: reqToSync.method,
63
72
  headers: reqToSync.headers,
64
- body: reqToSync.body
73
+ body: reqToSync.body,
74
+ signal: controller.signal
65
75
  });
76
+ clearTimeout(timeoutId);
66
77
  if (response.ok || response.status >= 400 && response.status < 500) {
67
78
  await this.storage.remove(reqToSync.id);
68
79
  console.log(`[Axiom] Request ${reqToSync.id} synced successfully.`);
@@ -70,6 +81,7 @@ var SyncManager = class {
70
81
  await this.handleFailure(reqToSync);
71
82
  }
72
83
  } catch (error) {
84
+ clearTimeout(timeoutId);
73
85
  await this.handleFailure(reqToSync);
74
86
  }
75
87
  }
@@ -78,7 +90,7 @@ var SyncManager = class {
78
90
  */
79
91
  async handleFailure(request) {
80
92
  request.retryCount += 1;
81
- const maxRetries = this.config.maxRetries || 3;
93
+ const maxRetries = this.config.maxRetries ?? 3;
82
94
  if (request.retryCount >= maxRetries) {
83
95
  console.warn(`[Axiom] Request ${request.id} failed ${maxRetries} times. Moving to Dead Letter.`);
84
96
  await this.storage.remove(request.id);
@@ -98,7 +110,10 @@ var AxiomEngine = class {
98
110
  this.storage = new MemoryStorageAdapter();
99
111
  }
100
112
  /**
101
- * Initializes the Axiom engine with your specific rules and storage.
113
+ * Initializes the Axiom engine with global configuration and a storage adapter.
114
+ * This must be called before making any requests to enable persistence.
115
+ * * @param config - Global configuration (baseURL, timeouts, custom headers, etc.)
116
+ * @param storageAdapter - Optional custom adapter (e.g., MMKV). Defaults to in-memory storage.
102
117
  */
103
118
  create(config, storageAdapter) {
104
119
  this.config = config;
@@ -107,6 +122,10 @@ var AxiomEngine = class {
107
122
  }
108
123
  this.syncManager = new SyncManager(this.storage, this.config);
109
124
  }
125
+ /**
126
+ * Manually triggers the background sync manager to flush all pending queued requests.
127
+ * Note: This is automatically handled by `AxiomProvider` when the network reconnects.
128
+ */
110
129
  async forceSync() {
111
130
  if (!this.syncManager) {
112
131
  console.error("[Axiom] Engine not initialized. Call axiom.create() first.");
@@ -115,59 +134,94 @@ var AxiomEngine = class {
115
134
  await this.syncManager.flushQueue();
116
135
  }
117
136
  /**
118
- * Generates a unique ID for queued requests.
137
+ * Generates a unique collision-resistant ID for queued requests.
119
138
  */
120
139
  generateId() {
121
140
  return Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
122
141
  }
123
142
  /**
124
- * The core POST method.
143
+ * Executes an HTTP GET request.
144
+ * If the network is unavailable or times out, the request is safely queued.
145
+ * * @param url - The endpoint URL (appended to baseURL if configured).
146
+ * @param options - Request-specific options (priority lanes, timeout overrides).
147
+ * @returns A promise resolving to the response data, status code, and queue state.
148
+ */
149
+ async get(url, options) {
150
+ return this.prepareRequest("GET", url, void 0, options);
151
+ }
152
+ /**
153
+ * Executes an HTTP POST request.
154
+ * If the network is unavailable or times out, the payload is safely queued.
155
+ * * @param url - The endpoint URL.
156
+ * @param data - The payload object to be serialized and sent.
157
+ * @param options - Request-specific options.
125
158
  */
126
159
  async post(url, data, options) {
127
- const fullUrl = this.config.baseURL ? `${this.config.baseURL}${url}` : url;
128
- const request = {
129
- id: this.generateId(),
130
- timestamp: Date.now(),
131
- url: fullUrl,
132
- method: "POST",
133
- headers: {
134
- "Content-Type": "application/json",
135
- ...this.config.defaultHeaders || {}
136
- },
137
- body: data ? JSON.stringify(data) : void 0,
138
- priority: options?.priority || "urgent",
139
- retryCount: 0
140
- };
141
- return this.attemptFetch(request);
160
+ return this.prepareRequest("POST", url, data, options);
142
161
  }
143
- /** The core GET method.
144
- * Note: GET requests typically don't have a body, but we still want to queue them if offline.
162
+ /**
163
+ * Executes an HTTP PUT request to entirely replace a resource.
164
+ * * @param url - The endpoint URL.
165
+ * @param data - The payload object to be serialized and sent.
166
+ * @param options - Request-specific options.
145
167
  */
146
- async get(url, options) {
168
+ async put(url, data, options) {
169
+ return this.prepareRequest("PUT", url, data, options);
170
+ }
171
+ /**
172
+ * Executes an HTTP PATCH request to partially update a resource.
173
+ * * @param url - The endpoint URL.
174
+ * @param data - The partial payload object to be serialized and sent.
175
+ * @param options - Request-specific options.
176
+ */
177
+ async patch(url, data, options) {
178
+ return this.prepareRequest("PATCH", url, data, options);
179
+ }
180
+ /**
181
+ * Executes an HTTP DELETE request.
182
+ * * @param url - The endpoint URL.
183
+ * @param options - Request-specific options.
184
+ */
185
+ async delete(url, options) {
186
+ return this.prepareRequest("DELETE", url, void 0, options);
187
+ }
188
+ /**
189
+ * Internal helper to consolidate request preparation and keep the engine DRY.
190
+ */
191
+ async prepareRequest(method, url, data, options) {
147
192
  const fullUrl = this.config.baseURL ? `${this.config.baseURL}${url}` : url;
193
+ const headers = { ...this.config.defaultHeaders || {} };
194
+ if (options?.headers) {
195
+ Object.assign(headers, options.headers);
196
+ }
148
197
  const request = {
149
198
  id: this.generateId(),
150
199
  timestamp: Date.now(),
151
200
  url: fullUrl,
152
- method: "GET",
153
- headers: {
154
- ...this.config.defaultHeaders || {}
155
- },
201
+ method,
202
+ headers,
203
+ body: data ? JSON.stringify(data) : void 0,
156
204
  priority: options?.priority || "urgent",
157
205
  retryCount: 0
158
206
  };
159
- return this.attemptFetch(request);
207
+ const timeoutMs = options?.timeout || this.config.timeout || 8e3;
208
+ return this.attemptFetch(request, timeoutMs);
160
209
  }
161
210
  /**
162
211
  * Internal logic to fire the request or catch the network drop.
212
+ * Handles timeout cancellations via AbortController.
163
213
  */
164
- async attemptFetch(request) {
214
+ async attemptFetch(request, timeoutMs) {
215
+ const controller = new AbortController();
216
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
165
217
  try {
166
218
  const response = await fetch(request.url, {
167
219
  method: request.method,
168
220
  headers: request.headers,
169
- body: request.body
221
+ body: request.body,
222
+ signal: controller.signal
170
223
  });
224
+ clearTimeout(timeoutId);
171
225
  if (response.ok) {
172
226
  const responseData = await response.json().catch(() => null);
173
227
  return { data: responseData, status: response.status, isQueued: false };
@@ -177,12 +231,16 @@ var AxiomEngine = class {
177
231
  }
178
232
  return { status: response.status, isQueued: false };
179
233
  } catch (error) {
234
+ clearTimeout(timeoutId);
235
+ if (error.name === "AbortError") {
236
+ console.warn(`[Axiom] Request to ${request.url} timed out after ${timeoutMs}ms. Queuing for retry.`);
237
+ }
180
238
  await this.enqueueRequest(request);
181
239
  return { status: 202, isQueued: true };
182
240
  }
183
241
  }
184
242
  /**
185
- * Saves the request to whatever storage adapter was provided on startup.
243
+ * Saves the request to the configured storage adapter.
186
244
  */
187
245
  async enqueueRequest(request) {
188
246
  console.warn(`[Axiom] Network unreachable. Queuing request ${request.id}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jayethian/axiom",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "maintainers": [
5
5
  "Jayetheus"
6
6
  ],
@@ -1,4 +1,4 @@
1
- import { AxiomConfig, QueuedRequest, RequestPriority } from '../types';
1
+ import { AxiomConfig, AxiomRequestOptions, QueuedRequest, RequestPriority } from '../types';
2
2
  import { AxiomStorageAdapter } from '../adapters';
3
3
  import { MemoryStorageAdapter } from '../adapters/memory';
4
4
  import { SyncManager } from './sync';
@@ -9,7 +9,10 @@ export class AxiomEngine {
9
9
  private syncManager!: SyncManager;
10
10
 
11
11
  /**
12
- * Initializes the Axiom engine with your specific rules and storage.
12
+ * Initializes the Axiom engine with global configuration and a storage adapter.
13
+ * This must be called before making any requests to enable persistence.
14
+ * * @param config - Global configuration (baseURL, timeouts, custom headers, etc.)
15
+ * @param storageAdapter - Optional custom adapter (e.g., MMKV). Defaults to in-memory storage.
13
16
  */
14
17
  public create(config: AxiomConfig, storageAdapter?: AxiomStorageAdapter): void {
15
18
  this.config = config;
@@ -20,6 +23,10 @@ export class AxiomEngine {
20
23
  this.syncManager = new SyncManager(this.storage, this.config);
21
24
  }
22
25
 
26
+ /**
27
+ * Manually triggers the background sync manager to flush all pending queued requests.
28
+ * Note: This is automatically handled by `AxiomProvider` when the network reconnects.
29
+ */
23
30
  public async forceSync(): Promise<void> {
24
31
  if (!this.syncManager) {
25
32
  console.error("[Axiom] Engine not initialized. Call axiom.create() first.");
@@ -29,98 +36,157 @@ export class AxiomEngine {
29
36
  }
30
37
 
31
38
  /**
32
- * Generates a unique ID for queued requests.
39
+ * Generates a unique collision-resistant ID for queued requests.
33
40
  */
34
41
  private generateId(): string {
35
42
  return Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
36
43
  }
37
44
 
38
45
  /**
39
- * The core POST method.
46
+ * Executes an HTTP GET request.
47
+ * If the network is unavailable or times out, the request is safely queued.
48
+ * * @param url - The endpoint URL (appended to baseURL if configured).
49
+ * @param options - Request-specific options (priority lanes, timeout overrides).
50
+ * @returns A promise resolving to the response data, status code, and queue state.
51
+ */
52
+ public async get<T>(
53
+ url: string,
54
+ options?: AxiomRequestOptions
55
+ ): Promise<{ data?: T; status: number; isQueued: boolean }> {
56
+ return this.prepareRequest<T>('GET', url, undefined, options);
57
+ }
58
+
59
+ /**
60
+ * Executes an HTTP POST request.
61
+ * If the network is unavailable or times out, the payload is safely queued.
62
+ * * @param url - The endpoint URL.
63
+ * @param data - The payload object to be serialized and sent.
64
+ * @param options - Request-specific options.
40
65
  */
41
66
  public async post<T>(
42
67
  url: string,
43
68
  data?: any,
44
- options?: { priority?: RequestPriority }
69
+ options?: AxiomRequestOptions
45
70
  ): Promise<{ data?: T; status: number; isQueued: boolean }> {
46
-
47
- const fullUrl = this.config.baseURL ? `${this.config.baseURL}${url}` : url;
48
-
49
- const request: QueuedRequest = {
50
- id: this.generateId(),
51
- timestamp: Date.now(),
52
- url: fullUrl,
53
- method: 'POST',
54
- headers: {
55
- 'Content-Type': 'application/json',
56
- ...(this.config.defaultHeaders || {})
57
- },
58
- body: data ? JSON.stringify(data) : undefined,
59
- priority: options?.priority || 'urgent',
60
- retryCount: 0
61
- };
71
+ return this.prepareRequest<T>('POST', url, data, options);
72
+ }
73
+
74
+ /**
75
+ * Executes an HTTP PUT request to entirely replace a resource.
76
+ * * @param url - The endpoint URL.
77
+ * @param data - The payload object to be serialized and sent.
78
+ * @param options - Request-specific options.
79
+ */
80
+ public async put<T>(
81
+ url: string,
82
+ data?: any,
83
+ options?: AxiomRequestOptions
84
+ ): Promise<{ data?: T; status: number; isQueued: boolean }> {
85
+ return this.prepareRequest<T>('PUT', url, data, options);
86
+ }
62
87
 
63
- return this.attemptFetch<T>(request);
88
+ /**
89
+ * Executes an HTTP PATCH request to partially update a resource.
90
+ * * @param url - The endpoint URL.
91
+ * @param data - The partial payload object to be serialized and sent.
92
+ * @param options - Request-specific options.
93
+ */
94
+ public async patch<T>(
95
+ url: string,
96
+ data?: any,
97
+ options?: AxiomRequestOptions
98
+ ): Promise<{ data?: T; status: number; isQueued: boolean }> {
99
+ return this.prepareRequest<T>('PATCH', url, data, options);
64
100
  }
65
101
 
66
- /** The core GET method.
67
- * Note: GET requests typically don't have a body, but we still want to queue them if offline.
102
+ /**
103
+ * Executes an HTTP DELETE request.
104
+ * * @param url - The endpoint URL.
105
+ * @param options - Request-specific options.
68
106
  */
69
- public async get<T>(
107
+ public async delete<T>(
70
108
  url: string,
71
- options?: { priority?: RequestPriority }
109
+ options?: AxiomRequestOptions
110
+ ): Promise<{ data?: T; status: number; isQueued: boolean }> {
111
+ return this.prepareRequest<T>('DELETE', url, undefined, options);
112
+ }
113
+
114
+ /**
115
+ * Internal helper to consolidate request preparation and keep the engine DRY.
116
+ */
117
+ private async prepareRequest<T>(
118
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
119
+ url: string,
120
+ data?: any,
121
+ options?: AxiomRequestOptions
72
122
  ): Promise<{ data?: T; status: number; isQueued: boolean }> {
73
-
74
123
  const fullUrl = this.config.baseURL ? `${this.config.baseURL}${url}` : url;
75
124
 
125
+ const headers: Record<string, string> = { ...(this.config.defaultHeaders || {}) };
126
+ if (options?.headers) {
127
+ Object.assign(headers, options.headers);
128
+ }
129
+
76
130
  const request: QueuedRequest = {
77
131
  id: this.generateId(),
78
132
  timestamp: Date.now(),
79
133
  url: fullUrl,
80
- method: 'GET',
81
- headers: {
82
- ...(this.config.defaultHeaders || {})
83
- },
134
+ method,
135
+ headers,
136
+ body: data ? JSON.stringify(data) : undefined,
84
137
  priority: options?.priority || 'urgent',
85
138
  retryCount: 0
86
139
  };
87
140
 
88
- return this.attemptFetch<T>(request);
141
+ const timeoutMs = options?.timeout || this.config.timeout || 8000;
142
+
143
+ return this.attemptFetch<T>(request, timeoutMs);
89
144
  }
90
145
 
91
146
  /**
92
147
  * Internal logic to fire the request or catch the network drop.
148
+ * Handles timeout cancellations via AbortController.
93
149
  */
94
- private async attemptFetch<T>(request: QueuedRequest): Promise<{ data?: T; status: number; isQueued: boolean }> {
150
+ private async attemptFetch<T>(request: QueuedRequest, timeoutMs: number): Promise<{ data?: T; status: number; isQueued: boolean }> {
151
+ const controller = new AbortController();
152
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
153
+
95
154
  try {
96
155
  const response = await fetch(request.url, {
97
156
  method: request.method,
98
157
  headers: request.headers,
99
- body: request.body
158
+ body: request.body,
159
+ signal: controller.signal
100
160
  });
101
161
 
162
+ clearTimeout(timeoutId);
163
+
102
164
  if (response.ok) {
103
165
  const responseData = await response.json().catch(() => null);
104
166
  return { data: responseData, status: response.status, isQueued: false };
105
167
  }
106
168
 
107
-
108
169
  if (response.status >= 500) {
109
170
  throw new Error('Server Error');
110
171
  }
111
172
 
112
-
113
173
  return { status: response.status, isQueued: false };
114
174
 
115
- } catch (error) {
116
- await this.enqueueRequest(request);
175
+ } catch (error: any) {
176
+ clearTimeout(timeoutId);
177
+
178
+ if(error.name === 'AbortError') {
179
+ console.warn(`[Axiom] Request to ${request.url} timed out after ${timeoutMs}ms. Queuing for retry.`);
180
+ }
181
+
182
+ await this.enqueueRequest(request);
117
183
 
118
- return { status: 202, isQueued: true };
184
+ return { status: 202, isQueued: true };
119
185
  }
120
186
  }
121
187
 
122
188
  /**
123
- * Saves the request to whatever storage adapter was provided on startup.
189
+ * Saves the request to the configured storage adapter.
124
190
  */
125
191
  private async enqueueRequest(request: QueuedRequest): Promise<void> {
126
192
  console.warn(`[Axiom] Network unreachable. Queuing request ${request.id}`);
@@ -11,9 +11,9 @@ export class SyncManager {
11
11
 
12
12
  /**
13
13
  * The master trigger. Call this when the OS reports network is back online.
14
+ * Automatically sorts requests so 'urgent' items bypass 'background' items.
14
15
  */
15
16
  public async flushQueue(): Promise<void> {
16
- // Prevent overlapping syncs if the network toggles rapidly
17
17
  if (this.isSyncing) return;
18
18
  this.isSyncing = true;
19
19
 
@@ -21,14 +21,20 @@ export class SyncManager {
21
21
  const pending = await this.storage.getAll();
22
22
 
23
23
  if (pending.length === 0) {
24
- this.isSyncing = false;
25
24
  return;
26
25
  }
27
26
 
28
27
  console.log(`[Axiom] Network restored. Syncing ${pending.length} queued requests...`);
29
28
 
30
- // We process sequentially to maintain the order of user actions
31
- for (const request of pending) {
29
+ // MITIGATION 3: Priority Lanes (Urgent requests jump the line)
30
+ // If priorities match, it falls back to timestamp (FIFO) to maintain action order.
31
+ const sortedQueue = pending.sort((a, b) => {
32
+ if (a.priority === 'urgent' && b.priority !== 'urgent') return -1;
33
+ if (a.priority !== 'urgent' && b.priority === 'urgent') return 1;
34
+ return a.timestamp - b.timestamp;
35
+ });
36
+
37
+ for (const request of sortedQueue) {
32
38
  await this.processRequest(request);
33
39
  }
34
40
 
@@ -48,18 +54,27 @@ export class SyncManager {
48
54
  try {
49
55
  reqToSync = await this.config.onBeforeSync(request);
50
56
  } catch (error) {
51
- console.error(`[Axiom] onBeforeSync failed for ${request.id}. Skipping.`);
57
+ console.error(`[Axiom] onBeforeSync failed for ${request.id}. Marking as failure.`);
58
+ await this.handleFailure(request); // FIX: Ensure we increment retry count if this fails
52
59
  return;
53
60
  }
54
61
  }
55
62
 
63
+ // Apply the global timeout to background syncing as well
64
+ const controller = new AbortController();
65
+ const timeoutMs = this.config.timeout || 10000; // Default 10s for background syncs
66
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
67
+
56
68
  try {
57
69
  const response = await fetch(reqToSync.url, {
58
70
  method: reqToSync.method,
59
71
  headers: reqToSync.headers,
60
- body: reqToSync.body
72
+ body: reqToSync.body,
73
+ signal: controller.signal
61
74
  });
62
75
 
76
+ clearTimeout(timeoutId);
77
+
63
78
  // If it's a success OR a permanent 400 error (like bad data), remove it.
64
79
  if (response.ok || (response.status >= 400 && response.status < 500)) {
65
80
  await this.storage.remove(reqToSync.id);
@@ -69,7 +84,8 @@ export class SyncManager {
69
84
  await this.handleFailure(reqToSync);
70
85
  }
71
86
  } catch (error) {
72
- // Network dropped again mid-sync.
87
+ clearTimeout(timeoutId);
88
+ // Network dropped again mid-sync or timeout was reached.
73
89
  await this.handleFailure(reqToSync);
74
90
  }
75
91
  }
@@ -79,7 +95,7 @@ export class SyncManager {
79
95
  */
80
96
  private async handleFailure(request: QueuedRequest): Promise<void> {
81
97
  request.retryCount += 1;
82
- const maxRetries = this.config.maxRetries || 3;
98
+ const maxRetries = this.config.maxRetries ?? 3; // Use nullish coalescing so 0 is valid
83
99
 
84
100
  if (request.retryCount >= maxRetries) {
85
101
  console.warn(`[Axiom] Request ${request.id} failed ${maxRetries} times. Moving to Dead Letter.`);
package/src/types.ts CHANGED
@@ -1,5 +1,10 @@
1
+ /** * Defines how the request should be treated when the queue flushes.
2
+ * Urgent requests bypass the background queue entirely if the network is active.
3
+ */
1
4
  export type RequestPriority = 'urgent' | 'background';
2
5
 
6
+ /** * Represents a serialized HTTP request frozen in offline storage.
7
+ */
3
8
  export interface QueuedRequest {
4
9
  id: string;
5
10
  timestamp: number;
@@ -11,10 +16,30 @@ export interface QueuedRequest {
11
16
  retryCount: number;
12
17
  }
13
18
 
19
+ /** * Global configuration for the Axiom engine initialized on startup.
20
+ */
14
21
  export interface AxiomConfig {
22
+ /** The base URL prepended to all request paths. */
15
23
  baseURL?: string;
24
+ /** Global headers applied to every request (e.g., Auth tokens). */
16
25
  defaultHeaders?: Record<string, string>;
26
+ /** The maximum number of times a queued request will attempt to sync before failing permanently. */
17
27
  maxRetries?: number;
28
+ /** Global timeout in milliseconds before a request is aborted and queued. */
29
+ timeout?: number;
30
+ /** Middleware hook triggered immediately before a queued request is synced. Ideal for refreshing Auth tokens. */
18
31
  onBeforeSync?: (request: QueuedRequest) => Promise<QueuedRequest>;
32
+ /** Callback triggered when a request exceeds maxRetries and is permanently removed from the queue. */
19
33
  onDeadLetter?: (request: QueuedRequest, error: Error) => void;
34
+ }
35
+
36
+ /** * Per-request configuration options that override the global configuration.
37
+ */
38
+ export interface AxiomRequestOptions {
39
+ /** Overrides the queue sorting behavior for this specific request. */
40
+ priority?: RequestPriority;
41
+ /** Overrides the global timeout limit for this specific request. */
42
+ timeout?: number;
43
+ /** Specific headers to append to this single request (e.g., custom Content-Type). */
44
+ headers?: Record<string, string>;
20
45
  }