@jayethian/axiom 0.1.0 → 0.1.2
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 +356 -1
- package/dist/index.d.mts +85 -13
- package/dist/index.d.ts +85 -13
- package/dist/index.js +92 -34
- package/dist/index.mjs +92 -34
- package/package.json +26 -5
- package/.editorconfig +0 -8
- package/.gitattributes +0 -4
- package/src/adapters/index.ts +0 -15
- package/src/adapters/memory.ts +0 -22
- package/src/engine/fetcher.ts +0 -131
- package/src/engine/sync.ts +0 -96
- package/src/index.ts +0 -6
- package/src/react/AxiomProvider.tsx +0 -57
- package/src/react/useAxiomQueue.ts +0 -12
- package/src/types.ts +0 -20
- package/tsconfig.json +0 -16
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
|
-
|
|
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}.
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
174
|
-
*
|
|
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
|
|
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
|
|
183
|
-
headers
|
|
184
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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}.
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
144
|
-
*
|
|
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
|
|
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
|
|
153
|
-
headers
|
|
154
|
-
|
|
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
|
-
|
|
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
|
|
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,13 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jayethian/axiom",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"maintainers": [
|
|
5
5
|
"Jayetheus"
|
|
6
6
|
],
|
|
7
|
-
"description": "A resilient, offline-first fetch wrapper",
|
|
7
|
+
"description": "A resilient, offline-first fetch wrapper for React Native & Next.js.",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"react-native",
|
|
10
|
+
"nextjs",
|
|
11
|
+
"fetch",
|
|
12
|
+
"axios",
|
|
13
|
+
"offline-first",
|
|
14
|
+
"sync",
|
|
15
|
+
"network",
|
|
16
|
+
"queue",
|
|
17
|
+
"indexeddb"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://github.com/jayethian/axiom#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/jayethian/axiom/issues"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/jayethian/axiom.git"
|
|
26
|
+
},
|
|
8
27
|
"publishConfig": {
|
|
9
28
|
"access": "public"
|
|
10
29
|
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
11
33
|
"main": "./dist/index.js",
|
|
12
34
|
"module": "./dist/index.mjs",
|
|
13
35
|
"types": "./dist/index.d.ts",
|
|
@@ -28,6 +50,5 @@
|
|
|
28
50
|
},
|
|
29
51
|
"peerDependencies": {
|
|
30
52
|
"react": ">=17.0.0"
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/.editorconfig
DELETED
package/.gitattributes
DELETED
package/src/adapters/index.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { QueuedRequest } from '../types';
|
|
2
|
-
|
|
3
|
-
export interface AxiomStorageAdapter {
|
|
4
|
-
/** Saves a request to the persistent queue */
|
|
5
|
-
save(request: QueuedRequest): Promise<void>;
|
|
6
|
-
|
|
7
|
-
/** Retrieves all pending requests, usually ordered by timestamp */
|
|
8
|
-
getAll(): Promise<QueuedRequest[]>;
|
|
9
|
-
|
|
10
|
-
/** Removes a specific request after it successfully syncs */
|
|
11
|
-
remove(id: string): Promise<void>;
|
|
12
|
-
|
|
13
|
-
/** Wipes the queue entirely (useful for user logout) */
|
|
14
|
-
clearAll(): Promise<void>;
|
|
15
|
-
}
|
package/src/adapters/memory.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { AxiomStorageAdapter } from './index';
|
|
2
|
-
import { QueuedRequest } from '../types';
|
|
3
|
-
|
|
4
|
-
export class MemoryStorageAdapter implements AxiomStorageAdapter {
|
|
5
|
-
private queue: Map<string, QueuedRequest> = new Map();
|
|
6
|
-
|
|
7
|
-
async save(request: QueuedRequest): Promise<void> {
|
|
8
|
-
this.queue.set(request.id, request);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
async getAll(): Promise<QueuedRequest[]> {
|
|
12
|
-
return Array.from(this.queue.values()).sort((a, b) => a.timestamp - b.timestamp);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async remove(id: string): Promise<void> {
|
|
16
|
-
this.queue.delete(id);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async clearAll(): Promise<void> {
|
|
20
|
-
this.queue.clear();
|
|
21
|
-
}
|
|
22
|
-
}
|
package/src/engine/fetcher.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { AxiomConfig, QueuedRequest, RequestPriority } from '../types';
|
|
2
|
-
import { AxiomStorageAdapter } from '../adapters';
|
|
3
|
-
import { MemoryStorageAdapter } from '../adapters/memory';
|
|
4
|
-
import { SyncManager } from './sync';
|
|
5
|
-
|
|
6
|
-
export class AxiomEngine {
|
|
7
|
-
private config: AxiomConfig = {};
|
|
8
|
-
private storage: AxiomStorageAdapter = new MemoryStorageAdapter();
|
|
9
|
-
private syncManager!: SyncManager;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Initializes the Axiom engine with your specific rules and storage.
|
|
13
|
-
*/
|
|
14
|
-
public create(config: AxiomConfig, storageAdapter?: AxiomStorageAdapter): void {
|
|
15
|
-
this.config = config;
|
|
16
|
-
if (storageAdapter) {
|
|
17
|
-
this.storage = storageAdapter;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
this.syncManager = new SyncManager(this.storage, this.config);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
public async forceSync(): Promise<void> {
|
|
24
|
-
if (!this.syncManager) {
|
|
25
|
-
console.error("[Axiom] Engine not initialized. Call axiom.create() first.");
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
await this.syncManager.flushQueue();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Generates a unique ID for queued requests.
|
|
33
|
-
*/
|
|
34
|
-
private generateId(): string {
|
|
35
|
-
return Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The core POST method.
|
|
40
|
-
*/
|
|
41
|
-
public async post<T>(
|
|
42
|
-
url: string,
|
|
43
|
-
data?: any,
|
|
44
|
-
options?: { priority?: RequestPriority }
|
|
45
|
-
): 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
|
-
};
|
|
62
|
-
|
|
63
|
-
return this.attemptFetch<T>(request);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** The core GET method.
|
|
67
|
-
* Note: GET requests typically don't have a body, but we still want to queue them if offline.
|
|
68
|
-
*/
|
|
69
|
-
public async get<T>(
|
|
70
|
-
url: string,
|
|
71
|
-
options?: { priority?: RequestPriority }
|
|
72
|
-
): Promise<{ data?: T; status: number; isQueued: boolean }> {
|
|
73
|
-
|
|
74
|
-
const fullUrl = this.config.baseURL ? `${this.config.baseURL}${url}` : url;
|
|
75
|
-
|
|
76
|
-
const request: QueuedRequest = {
|
|
77
|
-
id: this.generateId(),
|
|
78
|
-
timestamp: Date.now(),
|
|
79
|
-
url: fullUrl,
|
|
80
|
-
method: 'GET',
|
|
81
|
-
headers: {
|
|
82
|
-
...(this.config.defaultHeaders || {})
|
|
83
|
-
},
|
|
84
|
-
priority: options?.priority || 'urgent',
|
|
85
|
-
retryCount: 0
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
return this.attemptFetch<T>(request);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Internal logic to fire the request or catch the network drop.
|
|
93
|
-
*/
|
|
94
|
-
private async attemptFetch<T>(request: QueuedRequest): Promise<{ data?: T; status: number; isQueued: boolean }> {
|
|
95
|
-
try {
|
|
96
|
-
const response = await fetch(request.url, {
|
|
97
|
-
method: request.method,
|
|
98
|
-
headers: request.headers,
|
|
99
|
-
body: request.body
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
if (response.ok) {
|
|
103
|
-
const responseData = await response.json().catch(() => null);
|
|
104
|
-
return { data: responseData, status: response.status, isQueued: false };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (response.status >= 500) {
|
|
109
|
-
throw new Error('Server Error');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return { status: response.status, isQueued: false };
|
|
114
|
-
|
|
115
|
-
} catch (error) {
|
|
116
|
-
await this.enqueueRequest(request);
|
|
117
|
-
|
|
118
|
-
return { status: 202, isQueued: true };
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Saves the request to whatever storage adapter was provided on startup.
|
|
124
|
-
*/
|
|
125
|
-
private async enqueueRequest(request: QueuedRequest): Promise<void> {
|
|
126
|
-
console.warn(`[Axiom] Network unreachable. Queuing request ${request.id}`);
|
|
127
|
-
await this.storage.save(request);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export const axiom = new AxiomEngine();
|