@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 +150 -1
- package/assets/logo.png +0 -0
- 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 +1 -1
- package/src/engine/fetcher.ts +106 -40
- package/src/engine/sync.ts +24 -8
- package/src/types.ts +25 -0
package/README.md
CHANGED
|
@@ -1 +1,150 @@
|
|
|
1
|
-
|
|
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).
|
package/assets/logo.png
ADDED
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
65
|
-
*
|
|
131
|
+
/**
|
|
132
|
+
* Executes an HTTP DELETE request.
|
|
133
|
+
* * @param url - The endpoint URL.
|
|
134
|
+
* @param options - Request-specific options.
|
|
66
135
|
*/
|
|
67
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
65
|
-
*
|
|
131
|
+
/**
|
|
132
|
+
* Executes an HTTP DELETE request.
|
|
133
|
+
* * @param url - The endpoint URL.
|
|
134
|
+
* @param options - Request-specific options.
|
|
66
135
|
*/
|
|
67
|
-
|
|
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
|
|
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
|
-
|
|
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
package/src/engine/fetcher.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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?:
|
|
69
|
+
options?: AxiomRequestOptions
|
|
45
70
|
): Promise<{ data?: T; status: number; isQueued: boolean }> {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
67
|
-
*
|
|
102
|
+
/**
|
|
103
|
+
* Executes an HTTP DELETE request.
|
|
104
|
+
* * @param url - The endpoint URL.
|
|
105
|
+
* @param options - Request-specific options.
|
|
68
106
|
*/
|
|
69
|
-
public async
|
|
107
|
+
public async delete<T>(
|
|
70
108
|
url: string,
|
|
71
|
-
options?:
|
|
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
|
|
81
|
-
headers
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
+
return { status: 202, isQueued: true };
|
|
119
185
|
}
|
|
120
186
|
}
|
|
121
187
|
|
|
122
188
|
/**
|
|
123
|
-
* Saves the request to
|
|
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}`);
|
package/src/engine/sync.ts
CHANGED
|
@@ -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
|
-
//
|
|
31
|
-
|
|
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}.
|
|
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
|
-
|
|
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
|
|
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
|
}
|