@jayethian/axiom 0.1.1 β†’ 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 CHANGED
@@ -1,8 +1,8 @@
1
1
  <div align="center">
2
- <img src="./assets/logo.png" alt="axiom logo" width="400" />
2
+ <img src="https://raw.githubusercontent.com/jayethian/axiom/main/assets/logo.png" alt="axiom logo" width="400" />
3
3
  <h1>Axiom</h1>
4
4
 
5
- <h3>Resilient, offline-first networking for modern React apps.</h3>
5
+ <h3>Resilient, offline-first networking for modern React, Next.js, and React Native apps.</h3>
6
6
 
7
7
  <p>
8
8
  <a href="https://www.npmjs.com/package/@jayethian/axiom">
@@ -21,10 +21,10 @@
21
21
  <br />
22
22
 
23
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.**
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 written from scratch, **that data is gone forever.**
25
25
 
26
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.
27
+ Axiom intercepts network failures and timeouts. Instead of throwing an error, it serializes the request and safely moves it to a persistent local queue. When the connection returns, Axiom flushes the queue automatically, in the background.
28
28
 
29
29
  ```typescript
30
30
  // Standard Fetch: Fails and loses data when offline.
@@ -37,101 +37,242 @@ await axiom.post('/api/orders', data);
37
37
 
38
38
  ---
39
39
 
40
- ## Key Features
40
+ ## Table of Contents
41
41
 
42
- * **πŸ“± Mobile-First Resilience:** Specifically tuned for React Native's shaky connectivity.
42
+ 1. [Features](https://www.google.com/search?q=%23-features)
43
+ 2. [Installation](https://www.google.com/search?q=%23-installation)
44
+ 3. [React / Next.js Setup](https://www.google.com/search?q=%23-react--nextjs-setup-zero-config)
45
+ 4. [React Native Setup](https://www.google.com/search?q=%23-react-native-setup)
46
+ 5. [Vanilla JS / Node Setup](https://www.google.com/search?q=%23-vanilla-js--node-setup)
47
+ 6. [Core Hooks (`useAxiomQueue`)](https://www.google.com/search?q=%23-core-hooks)
48
+ 7. [Advanced Architecture](https://www.google.com/search?q=%23-advanced-architecture)
49
+ * [Global Interceptors](https://www.google.com/search?q=%23global-interceptors)
50
+ * [Priority Lanes](https://www.google.com/search?q=%23priority-lanes)
51
+ * [Queue Inspection & "Outbox" UI](https://www.google.com/search?q=%23queue-inspection--outbox-ui)
52
+ * [Storage Adapters (MMKV, IndexedDB)](https://www.google.com/search?q=%23storage-adapters)
53
+
54
+
55
+ 8. [API Reference](https://www.google.com/search?q=%23-api-reference)
56
+
57
+ ---
58
+
59
+ ## Features
60
+
61
+ * **πŸ“± Mobile-First Resilience:** Specifically tuned to handle spotty connectivity, aggressive timeouts, and background execution.
62
+ * **🧠 Smart Fallback Storage:** Automatically detects your environment and falls back to the safest storage (`IndexedDB` for Web, `Memory` for SSR/React Native) without crashing.
43
63
  * **πŸ”„ Autonomous Background Sync:** Replays the queue the moment a signal is detected.
44
64
  * **⚑ 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.
65
+ * **πŸ›‘οΈ Just-In-Time Headers:** Refresh Auth Tokens immediately before syncing to prevent `401 Unauthorized` errors on delayed requests.
66
+ * **πŸ”Œ Global Interceptors:** Catch success and error events globally, even when requests resolve in the background hours later.
67
+ * **πŸͺ¦ Dead Letter Queues:** Protects your app from infinite loops by isolating permanently failing requests and exposing them to the UI for user intervention.
48
68
 
49
69
  ---
50
70
 
51
71
  ## Installation
52
72
 
53
73
  ```bash
74
+ npm install @jayethian/axiom
75
+ # or
54
76
  yarn add @jayethian/axiom
55
77
  # or
56
- npm install @jayethian/axiom
78
+ pnpm add @jayethian/axiom
57
79
 
58
80
  ```
59
81
 
60
82
  ---
61
83
 
62
- ## Setup & Usage
84
+ ## React & Next.js Setup (Zero-Config)
85
+ Axiom includes a built-in event listener that automatically binds to the browser's `window.addEventListener('online')` APIs. For Next.js and standard React Web apps, setup requires zero boilerplate.
86
+
87
+ ```tsx
88
+ // App.tsx or layout.tsx
89
+ import { AxiomProvider } from '@jayethian/axiom';
90
+
91
+ export default function App({ children }) {
92
+ return (
93
+ <AxiomProvider
94
+ config={{
95
+ baseURL: '[https://api.myapp.com](https://api.myapp.com)',
96
+ timeout: 8000
97
+ }}
98
+ // Automatically uses IndexedDB, falls back to LocalStorage if in Private Browsing
99
+ fallbackAdapter="indexeddb"
100
+ >
101
+ {children}
102
+ </AxiomProvider>
103
+ );
104
+ }
105
+
106
+ ```
107
+
108
+ ---
63
109
 
64
- ### 1. Initialize the Provider
110
+ ## React Native Setup
65
111
 
66
- Wrap your app root. Axiom doesn't force a specific network library on youβ€”just pass in your preferred listener (like NetInfo).
112
+ React Native does not have a native DOM `window`, so you must provide a network listener (like `@react-native-community/netinfo`) and a persistent storage adapter (like `react-native-mmkv` or `AsyncStorage`).
67
113
 
68
114
  ```tsx
69
115
  import { AxiomProvider } from '@jayethian/axiom';
70
116
  import NetInfo from '@react-native-community/netinfo';
117
+ import { MMKVAdapter } from './my-adapters'; // See Storage Adapters below
71
118
 
72
- export default function App() {
119
+ export default function App({ children }) {
73
120
  return (
74
121
  <AxiomProvider
75
- config={{ baseURL: '[https://api.myapp.com](https://api.myapp.com)', timeout: 10000 }}
122
+ config={{ baseURL: '[https://api.myapp.com](https://api.myapp.com)' }}
123
+ storageAdapter={new MMKVAdapter()}
76
124
  networkListener={(callback) => {
77
125
  return NetInfo.addEventListener(state => callback(!!state.isConnected));
78
126
  }}
79
127
  >
80
- <Main />
128
+ {children}
81
129
  </AxiomProvider>
82
130
  );
83
131
  }
84
132
 
85
133
  ```
86
134
 
87
- ### 2. Use Hooks for Better UX
135
+ ---
136
+
137
+ ## Vanilla JS / Node Setup
138
+
139
+ You do not need React to use Axiom. You can instantiate the engine directly and use our built-in **Event Emitter** to listen for background syncs.
140
+
141
+ ```typescript
142
+ import { axiom } from '@jayethian/axiom';
143
+
144
+ // 1. Initialize
145
+ axiom.create({ baseURL: '[https://api.myapp.com](https://api.myapp.com)' });
88
146
 
89
- Keep your users informed when they are working offline.
147
+ // 2. Listen to Background Events
148
+ axiom.on('syncSuccess', (data, req) => {
149
+ console.log(`Background sync finished for ${req.url}`);
150
+ });
151
+
152
+ axiom.on('deadLetter', (req) => {
153
+ console.error(`Request permanently failed after 3 retries:`, req);
154
+ });
155
+
156
+ // 3. Make Requests
157
+ const response = await axiom.post('/users', { name: 'John' });
158
+
159
+ ```
160
+
161
+ ---
162
+
163
+ ## Core Hooks
164
+
165
+ The `useAxiomQueue` hook gives your UI complete visibility into the background engine. Keep your users informed when they are working offline.
90
166
 
91
167
  ```tsx
92
168
  import { axiom, useAxiomQueue } from '@jayethian/axiom';
93
169
 
94
- const { isOnline } = useAxiomQueue();
170
+ export function CheckoutButton() {
171
+ const { isOnline, deadLetters, clearDeadLetters } = useAxiomQueue();
95
172
 
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
- };
173
+ const onSave = async (data) => {
174
+ // If offline, returns a 202 and flags isQueued: true
175
+ const res = await axiom.post('/checkout', data, { priority: 'urgent' });
176
+
177
+ if (res.isQueued) {
178
+ alert("Working offline. Your order will sync automatically!");
179
+ }
180
+ };
181
+
182
+ return (
183
+ <div>
184
+ {!isOnline && <Banner>You are offline. Actions will be saved.</Banner>}
185
+ {deadLetters.length > 0 && <ErrorBanner>Some actions failed to save.</ErrorBanner>}
186
+
187
+ <button onClick={onSave}>Checkout</button>
188
+ </div>
189
+ );
190
+ }
103
191
 
104
192
  ```
105
193
 
106
194
  ---
107
195
 
108
- ## Advanced Configuration
196
+ ## Advanced Architecture
197
+
198
+ ### Global Interceptors
199
+
200
+ Axiom allows you to intercept requests exactly like Axios, but it applies these rules to **background syncs** as well.
201
+
202
+ ```tsx
203
+ <AxiomProvider
204
+ config={{
205
+ // Triggered globally whenever a request hard-fails (e.g., 500, 401)
206
+ onError: (status, error, request) => {
207
+ if (status === 401) {
208
+ AuthService.logout(); // Global logout on token expiration
209
+ }
210
+ },
211
+ // Triggered globally whenever ANY request succeeds (immediate or background)
212
+ onResponse: (data, status, request) => {
213
+ if (request.url.includes('/payment')) {
214
+ Analytics.track('Payment Successful');
215
+ }
216
+ }
217
+ }}
218
+ >
219
+
220
+ ```
109
221
 
110
222
  ### Priority Lanes
111
223
 
112
- Prevent large background uploads from blocking small, critical API calls.
224
+ By default, Axiom queues requests First-In-First-Out (FIFO). However, you can force critical requests to jump to the front of the line when the network returns.
113
225
 
114
226
  ```typescript
115
- // This stays at the back of the line
227
+ // This stays at the back of the line (queued in the background)
116
228
  axiom.post('/analytics', logData, { priority: 'background' });
117
229
 
118
- // This jumps to the front
230
+ // This jumps to the front and syncs first when the connection returns
119
231
  axiom.post('/chat/send', message, { priority: 'urgent' });
120
232
 
121
233
  ```
122
234
 
235
+ ### Queue Inspection & "Outbox" UI
236
+
237
+ Give your users the ability to see what is waiting to sync, and let them cancel actions before the network returns.
238
+
239
+ ```tsx
240
+ import { useAxiomQueue } from '@jayethian/axiom';
241
+
242
+ export function Outbox() {
243
+ const { inspectQueue, cancelRequest } = useAxiomQueue();
244
+ const [pending, setPending] = useState([]);
245
+
246
+ useEffect(() => {
247
+ inspectQueue().then(setPending);
248
+ }, []);
249
+
250
+ return (
251
+ <ul>
252
+ {pending.map(req => (
253
+ <li key={req.id}>
254
+ Pending: {req.method} {req.url}
255
+ <button onClick={() => cancelRequest(req.id)}>Cancel</button>
256
+ </li>
257
+ ))}
258
+ </ul>
259
+ );
260
+ }
261
+
262
+ ```
263
+
123
264
  ### Just-In-Time Headers
124
265
 
125
- Refresh your JWT right before the queue fires to ensure every request is authorized.
266
+ If a user is offline for 4 hours, their JWT will likely expire. If Axiom attempts to sync the old request, the server will reject it. `onBeforeSync` allows you to inject fresh tokens *milliseconds* before the queue flushes.
126
267
 
127
268
  ```tsx
128
269
  <AxiomProvider
129
270
  config={{
130
271
  onBeforeSync: async (request) => {
131
- const token = await getFreshToken();
272
+ const freshToken = await getValidAuthToken(); // Your logic
132
273
  return {
133
274
  ...request,
134
- headers: { ...request.headers, Authorization: `Bearer ${token}` }
275
+ headers: { ...request.headers, Authorization: `Bearer ${freshToken}` }
135
276
  };
136
277
  }
137
278
  }}
@@ -139,12 +280,77 @@ Refresh your JWT right before the queue fires to ensure every request is authori
139
280
 
140
281
  ```
141
282
 
283
+ ### Storage Adapters
284
+
285
+ Axiom comes with `IndexedDB`, `LocalStorage`, and `Memory` adapters out of the box. For React Native, building a custom adapter using a high-performance library like MMKV is incredibly simple.
286
+
287
+ ```typescript
288
+ import { MMKV } from 'react-native-mmkv';
289
+ import { AxiomStorageAdapter, QueuedRequest } from '@jayethian/axiom';
290
+
291
+ const mmkv = new MMKV();
292
+
293
+ export class MMKVAdapter implements AxiomStorageAdapter {
294
+ private key = 'axiom_queue';
295
+
296
+ private getQ(): QueuedRequest[] {
297
+ const data = mmkv.getString(this.key);
298
+ return data ? JSON.parse(data) : [];
299
+ }
300
+
301
+ async save(req: QueuedRequest) {
302
+ const q = this.getQ();
303
+ q.push(req);
304
+ mmkv.set(this.key, JSON.stringify(q));
305
+ }
306
+
307
+ async getAll() { return this.getQ(); }
308
+
309
+ async remove(id: string) {
310
+ const q = this.getQ().filter(r => r.id !== id);
311
+ mmkv.set(this.key, JSON.stringify(q));
312
+ }
313
+
314
+ async clearAll() { mmkv.delete(this.key); }
315
+ }
316
+
317
+ // Pass it to the provider:
318
+ <AxiomProvider storageAdapter={new MMKVAdapter()} {...props} />
319
+
320
+ ```
321
+
322
+ ---
323
+
324
+ ## API Reference
325
+
326
+ ### `AxiomConfig`
327
+
328
+ | Property | Type | Default | Description |
329
+ | --- | --- | --- | --- |
330
+ | `baseURL` | `string` | `undefined` | Prepend this to all request URLs. |
331
+ | `defaultHeaders` | `Record<string, string>` | `{}` | Global headers applied to all requests. |
332
+ | `timeout` | `number` | `8000` | MS before a request is aborted and moved to the offline queue. |
333
+ | `maxRetries` | `number` | `3` | Attempts before a background sync fails permanently. |
334
+ | `fallbackAdapter` | `'indexeddb' | 'localstorage' | 'memory'` | `'memory'` | The internal adapter to use if `storageAdapter` is omitted. |
335
+ | `debug` | `boolean` | `false` | Prints verbose engine logs to the console. |
336
+
337
+ ### `AxiomRequestOptions`
338
+
339
+ Passed as the third parameter to `axiom.post`, `axiom.get`, etc.
340
+
341
+ | Property | Type | Description |
342
+ | --- | --- | --- |
343
+ | `priority` | `'urgent' | 'background'` | Determines sort order when the queue flushes. |
344
+ | `timeout` | `number` | Overrides the global timeout for this specific request. |
345
+ | `headers` | `Record<string, string>` | Append or overwrite global headers for this request. |
346
+ | `metadata` | `any` | Attach custom UI data to the request. Survives serialization. |
347
+
142
348
  ---
143
349
 
144
350
  ## Contributing
145
351
 
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).
352
+ Contributions, issues, and feature requests are welcome! Feel free to check the [issues page](https://www.google.com/search?q=https://github.com/jayethian/axiom/issues).
147
353
 
148
- ## License
354
+ ## πŸ“„ License
149
355
 
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).
356
+ This project is [MIT](https://opensource.org/licenses/MIT) licensed. Built with ⚑️ by [Jayetheus](https://www.google.com/search?q=https://github.com/jayethian).
package/package.json CHANGED
@@ -1,13 +1,35 @@
1
1
  {
2
2
  "name": "@jayethian/axiom",
3
- "version": "0.1.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
- "packageManager": "yarn@4.12.0"
33
- }
53
+ }
54
+ }
package/.editorconfig DELETED
@@ -1,8 +0,0 @@
1
- root = true
2
-
3
- [*]
4
- charset = utf-8
5
- end_of_line = lf
6
- indent_size = 2
7
- indent_style = space
8
- insert_final_newline = true
package/.gitattributes DELETED
@@ -1,4 +0,0 @@
1
- /.yarn/** linguist-vendored
2
- /.yarn/releases/* binary
3
- /.yarn/plugins/**/* binary
4
- /.pnp.* binary linguist-generated
package/assets/logo.png DELETED
Binary file
@@ -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
- }
@@ -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
- }
@@ -1,197 +0,0 @@
1
- import { AxiomConfig, AxiomRequestOptions, 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 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.
16
- */
17
- public create(config: AxiomConfig, storageAdapter?: AxiomStorageAdapter): void {
18
- this.config = config;
19
- if (storageAdapter) {
20
- this.storage = storageAdapter;
21
- }
22
-
23
- this.syncManager = new SyncManager(this.storage, this.config);
24
- }
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
- */
30
- public async forceSync(): Promise<void> {
31
- if (!this.syncManager) {
32
- console.error("[Axiom] Engine not initialized. Call axiom.create() first.");
33
- return;
34
- }
35
- await this.syncManager.flushQueue();
36
- }
37
-
38
- /**
39
- * Generates a unique collision-resistant ID for queued requests.
40
- */
41
- private generateId(): string {
42
- return Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
43
- }
44
-
45
- /**
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.
65
- */
66
- public async post<T>(
67
- url: string,
68
- data?: any,
69
- options?: AxiomRequestOptions
70
- ): Promise<{ data?: T; status: number; isQueued: boolean }> {
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
- }
87
-
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);
100
- }
101
-
102
- /**
103
- * Executes an HTTP DELETE request.
104
- * * @param url - The endpoint URL.
105
- * @param options - Request-specific options.
106
- */
107
- public async delete<T>(
108
- url: string,
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
122
- ): Promise<{ data?: T; status: number; isQueued: boolean }> {
123
- const fullUrl = this.config.baseURL ? `${this.config.baseURL}${url}` : url;
124
-
125
- const headers: Record<string, string> = { ...(this.config.defaultHeaders || {}) };
126
- if (options?.headers) {
127
- Object.assign(headers, options.headers);
128
- }
129
-
130
- const request: QueuedRequest = {
131
- id: this.generateId(),
132
- timestamp: Date.now(),
133
- url: fullUrl,
134
- method,
135
- headers,
136
- body: data ? JSON.stringify(data) : undefined,
137
- priority: options?.priority || 'urgent',
138
- retryCount: 0
139
- };
140
-
141
- const timeoutMs = options?.timeout || this.config.timeout || 8000;
142
-
143
- return this.attemptFetch<T>(request, timeoutMs);
144
- }
145
-
146
- /**
147
- * Internal logic to fire the request or catch the network drop.
148
- * Handles timeout cancellations via AbortController.
149
- */
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
-
154
- try {
155
- const response = await fetch(request.url, {
156
- method: request.method,
157
- headers: request.headers,
158
- body: request.body,
159
- signal: controller.signal
160
- });
161
-
162
- clearTimeout(timeoutId);
163
-
164
- if (response.ok) {
165
- const responseData = await response.json().catch(() => null);
166
- return { data: responseData, status: response.status, isQueued: false };
167
- }
168
-
169
- if (response.status >= 500) {
170
- throw new Error('Server Error');
171
- }
172
-
173
- return { status: response.status, isQueued: false };
174
-
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);
183
-
184
- return { status: 202, isQueued: true };
185
- }
186
- }
187
-
188
- /**
189
- * Saves the request to the configured storage adapter.
190
- */
191
- private async enqueueRequest(request: QueuedRequest): Promise<void> {
192
- console.warn(`[Axiom] Network unreachable. Queuing request ${request.id}`);
193
- await this.storage.save(request);
194
- }
195
- }
196
-
197
- export const axiom = new AxiomEngine();
@@ -1,112 +0,0 @@
1
- import { AxiomStorageAdapter } from '../adapters';
2
- import { AxiomConfig, QueuedRequest } from '../types';
3
-
4
- export class SyncManager {
5
- private isSyncing = false;
6
-
7
- constructor(
8
- private storage: AxiomStorageAdapter,
9
- private config: AxiomConfig
10
- ) {}
11
-
12
- /**
13
- * The master trigger. Call this when the OS reports network is back online.
14
- * Automatically sorts requests so 'urgent' items bypass 'background' items.
15
- */
16
- public async flushQueue(): Promise<void> {
17
- if (this.isSyncing) return;
18
- this.isSyncing = true;
19
-
20
- try {
21
- const pending = await this.storage.getAll();
22
-
23
- if (pending.length === 0) {
24
- return;
25
- }
26
-
27
- console.log(`[Axiom] Network restored. Syncing ${pending.length} queued requests...`);
28
-
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) {
38
- await this.processRequest(request);
39
- }
40
-
41
- } finally {
42
- this.isSyncing = false;
43
- }
44
- }
45
-
46
- /**
47
- * Attempts to execute a single saved request.
48
- */
49
- private async processRequest(request: QueuedRequest): Promise<void> {
50
- let reqToSync = request;
51
-
52
- // MITIGATION 1: Just-in-Time Headers (Refresh Auth Tokens)
53
- if (this.config.onBeforeSync) {
54
- try {
55
- reqToSync = await this.config.onBeforeSync(request);
56
- } catch (error) {
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
59
- return;
60
- }
61
- }
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
-
68
- try {
69
- const response = await fetch(reqToSync.url, {
70
- method: reqToSync.method,
71
- headers: reqToSync.headers,
72
- body: reqToSync.body,
73
- signal: controller.signal
74
- });
75
-
76
- clearTimeout(timeoutId);
77
-
78
- // If it's a success OR a permanent 400 error (like bad data), remove it.
79
- if (response.ok || (response.status >= 400 && response.status < 500)) {
80
- await this.storage.remove(reqToSync.id);
81
- console.log(`[Axiom] Request ${reqToSync.id} synced successfully.`);
82
- } else {
83
- // It's a 500 Server Error. Treat as a failure and retry later.
84
- await this.handleFailure(reqToSync);
85
- }
86
- } catch (error) {
87
- clearTimeout(timeoutId);
88
- // Network dropped again mid-sync or timeout was reached.
89
- await this.handleFailure(reqToSync);
90
- }
91
- }
92
-
93
- /**
94
- * MITIGATION 2: The Dead Letter Queue logic
95
- */
96
- private async handleFailure(request: QueuedRequest): Promise<void> {
97
- request.retryCount += 1;
98
- const maxRetries = this.config.maxRetries ?? 3; // Use nullish coalescing so 0 is valid
99
-
100
- if (request.retryCount >= maxRetries) {
101
- console.warn(`[Axiom] Request ${request.id} failed ${maxRetries} times. Moving to Dead Letter.`);
102
- await this.storage.remove(request.id);
103
-
104
- if (this.config.onDeadLetter) {
105
- this.config.onDeadLetter(request, new Error('Max retries exceeded'));
106
- }
107
- } else {
108
- // Save the updated retry count back to storage
109
- await this.storage.save(request);
110
- }
111
- }
112
- }
package/src/index.ts DELETED
@@ -1,6 +0,0 @@
1
- export * from './types';
2
- export * from './adapters';
3
- export * from './adapters/memory';
4
- export { axiom, AxiomEngine } from './engine/fetcher';
5
- export { AxiomProvider } from './react/AxiomProvider';
6
- export { useAxiomQueue } from './react/useAxiomQueue';
@@ -1,57 +0,0 @@
1
- import React, { createContext, useContext, useEffect, useState } from 'react';
2
- import { axiom } from '../engine/fetcher';
3
- import { AxiomConfig } from '../types';
4
- import { AxiomStorageAdapter } from '../adapters';
5
-
6
- interface AxiomContextType {
7
- isOnline: boolean;
8
- forceSync: () => Promise<void>;
9
- }
10
-
11
- // Create a safe default context
12
- const AxiomContext = createContext<AxiomContextType>({
13
- isOnline: true,
14
- forceSync: async () => {},
15
- });
16
-
17
- export const AxiomProvider: React.FC<{
18
- config: AxiomConfig;
19
- storageAdapter?: AxiomStorageAdapter;
20
- /** * A function that takes a callback, calls it whenever network state changes,
21
- * and returns an unsubscribe function.
22
- */
23
- networkListener: (callback: (isOnline: boolean) => void) => any;
24
- children: React.ReactNode;
25
- }> = ({ config, storageAdapter, networkListener, children }) => {
26
- const [isOnline, setIsOnline] = useState(true);
27
-
28
- useEffect(() => {
29
- // 1. Boot up the Axiom engine
30
- axiom.create(config, storageAdapter);
31
-
32
- // 2. Attach the OS-level network listener
33
- const unsubscribe = networkListener((onlineStatus) => {
34
- setIsOnline(onlineStatus);
35
-
36
- // 3. The Magic: If the internet comes back, automatically flush the queue
37
- if (onlineStatus) {
38
- axiom.forceSync();
39
- }
40
- });
41
-
42
- // Cleanup listener on unmount
43
- return () => {
44
- if (typeof unsubscribe === 'function') {
45
- unsubscribe();
46
- }
47
- };
48
- }, [config, storageAdapter, networkListener]);
49
-
50
- return (
51
- <AxiomContext.Provider value={{ isOnline, forceSync: () => axiom.forceSync() }}>
52
- {children}
53
- </AxiomContext.Provider>
54
- );
55
- };
56
-
57
- export const useAxiomContext = () => useContext(AxiomContext);
@@ -1,12 +0,0 @@
1
- import { useAxiomContext } from './AxiomProvider';
2
-
3
- export function useAxiomQueue() {
4
- const { isOnline, forceSync } = useAxiomContext();
5
-
6
- return {
7
- /** Boolean indicating if the device currently has an active connection */
8
- isOnline,
9
- /** Manually trigger the background sync manager */
10
- forceSync,
11
- };
12
- }
package/src/types.ts DELETED
@@ -1,45 +0,0 @@
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
- */
4
- export type RequestPriority = 'urgent' | 'background';
5
-
6
- /** * Represents a serialized HTTP request frozen in offline storage.
7
- */
8
- export interface QueuedRequest {
9
- id: string;
10
- timestamp: number;
11
- url: string;
12
- method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
13
- headers: Record<string, string>;
14
- body?: string;
15
- priority: RequestPriority;
16
- retryCount: number;
17
- }
18
-
19
- /** * Global configuration for the Axiom engine initialized on startup.
20
- */
21
- export interface AxiomConfig {
22
- /** The base URL prepended to all request paths. */
23
- baseURL?: string;
24
- /** Global headers applied to every request (e.g., Auth tokens). */
25
- defaultHeaders?: Record<string, string>;
26
- /** The maximum number of times a queued request will attempt to sync before failing permanently. */
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. */
31
- onBeforeSync?: (request: QueuedRequest) => Promise<QueuedRequest>;
32
- /** Callback triggered when a request exceeds maxRetries and is permanently removed from the queue. */
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>;
45
- }
package/tsconfig.json DELETED
@@ -1,16 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "strict": true,
7
- "jsx": "react-jsx",
8
- "esModuleInterop": true,
9
- "skipLibCheck": true,
10
- "forceConsistentCasingInFileNames": true,
11
- "declaration": true,
12
- "ignoreDeprecations": "6.0"
13
- },
14
- "include": ["src/**/*"],
15
- "exclude": ["node_modules", "dist", "tests"]
16
- }