@jayethian/axiom 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.editorconfig ADDED
@@ -0,0 +1,8 @@
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 ADDED
@@ -0,0 +1,4 @@
1
+ /.yarn/** linguist-vendored
2
+ /.yarn/releases/* binary
3
+ /.yarn/plugins/**/* binary
4
+ /.pnp.* binary linguist-generated
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # axiom
@@ -0,0 +1,102 @@
1
+ import React from 'react';
2
+
3
+ type RequestPriority = 'urgent' | 'background';
4
+ interface QueuedRequest {
5
+ id: string;
6
+ timestamp: number;
7
+ url: string;
8
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
9
+ headers: Record<string, string>;
10
+ body?: string;
11
+ priority: RequestPriority;
12
+ retryCount: number;
13
+ }
14
+ interface AxiomConfig {
15
+ baseURL?: string;
16
+ defaultHeaders?: Record<string, string>;
17
+ maxRetries?: number;
18
+ onBeforeSync?: (request: QueuedRequest) => Promise<QueuedRequest>;
19
+ onDeadLetter?: (request: QueuedRequest, error: Error) => void;
20
+ }
21
+
22
+ interface AxiomStorageAdapter {
23
+ /** Saves a request to the persistent queue */
24
+ save(request: QueuedRequest): Promise<void>;
25
+ /** Retrieves all pending requests, usually ordered by timestamp */
26
+ getAll(): Promise<QueuedRequest[]>;
27
+ /** Removes a specific request after it successfully syncs */
28
+ remove(id: string): Promise<void>;
29
+ /** Wipes the queue entirely (useful for user logout) */
30
+ clearAll(): Promise<void>;
31
+ }
32
+
33
+ declare class MemoryStorageAdapter implements AxiomStorageAdapter {
34
+ private queue;
35
+ save(request: QueuedRequest): Promise<void>;
36
+ getAll(): Promise<QueuedRequest[]>;
37
+ remove(id: string): Promise<void>;
38
+ clearAll(): Promise<void>;
39
+ }
40
+
41
+ declare class AxiomEngine {
42
+ private config;
43
+ private storage;
44
+ private syncManager;
45
+ /**
46
+ * Initializes the Axiom engine with your specific rules and storage.
47
+ */
48
+ create(config: AxiomConfig, storageAdapter?: AxiomStorageAdapter): void;
49
+ forceSync(): Promise<void>;
50
+ /**
51
+ * Generates a unique ID for queued requests.
52
+ */
53
+ private generateId;
54
+ /**
55
+ * The core POST method.
56
+ */
57
+ post<T>(url: string, data?: any, options?: {
58
+ priority?: RequestPriority;
59
+ }): Promise<{
60
+ data?: T;
61
+ status: number;
62
+ isQueued: boolean;
63
+ }>;
64
+ /** The core GET method.
65
+ * Note: GET requests typically don't have a body, but we still want to queue them if offline.
66
+ */
67
+ get<T>(url: string, options?: {
68
+ priority?: RequestPriority;
69
+ }): Promise<{
70
+ data?: T;
71
+ status: number;
72
+ isQueued: boolean;
73
+ }>;
74
+ /**
75
+ * Internal logic to fire the request or catch the network drop.
76
+ */
77
+ private attemptFetch;
78
+ /**
79
+ * Saves the request to whatever storage adapter was provided on startup.
80
+ */
81
+ private enqueueRequest;
82
+ }
83
+ declare const axiom: AxiomEngine;
84
+
85
+ declare const AxiomProvider: React.FC<{
86
+ config: AxiomConfig;
87
+ storageAdapter?: AxiomStorageAdapter;
88
+ /** * A function that takes a callback, calls it whenever network state changes,
89
+ * and returns an unsubscribe function.
90
+ */
91
+ networkListener: (callback: (isOnline: boolean) => void) => any;
92
+ children: React.ReactNode;
93
+ }>;
94
+
95
+ declare function useAxiomQueue(): {
96
+ /** Boolean indicating if the device currently has an active connection */
97
+ isOnline: boolean;
98
+ /** Manually trigger the background sync manager */
99
+ forceSync: () => Promise<void>;
100
+ };
101
+
102
+ export { type AxiomConfig, AxiomEngine, AxiomProvider, type AxiomStorageAdapter, MemoryStorageAdapter, type QueuedRequest, type RequestPriority, axiom, useAxiomQueue };
@@ -0,0 +1,102 @@
1
+ import React from 'react';
2
+
3
+ type RequestPriority = 'urgent' | 'background';
4
+ interface QueuedRequest {
5
+ id: string;
6
+ timestamp: number;
7
+ url: string;
8
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
9
+ headers: Record<string, string>;
10
+ body?: string;
11
+ priority: RequestPriority;
12
+ retryCount: number;
13
+ }
14
+ interface AxiomConfig {
15
+ baseURL?: string;
16
+ defaultHeaders?: Record<string, string>;
17
+ maxRetries?: number;
18
+ onBeforeSync?: (request: QueuedRequest) => Promise<QueuedRequest>;
19
+ onDeadLetter?: (request: QueuedRequest, error: Error) => void;
20
+ }
21
+
22
+ interface AxiomStorageAdapter {
23
+ /** Saves a request to the persistent queue */
24
+ save(request: QueuedRequest): Promise<void>;
25
+ /** Retrieves all pending requests, usually ordered by timestamp */
26
+ getAll(): Promise<QueuedRequest[]>;
27
+ /** Removes a specific request after it successfully syncs */
28
+ remove(id: string): Promise<void>;
29
+ /** Wipes the queue entirely (useful for user logout) */
30
+ clearAll(): Promise<void>;
31
+ }
32
+
33
+ declare class MemoryStorageAdapter implements AxiomStorageAdapter {
34
+ private queue;
35
+ save(request: QueuedRequest): Promise<void>;
36
+ getAll(): Promise<QueuedRequest[]>;
37
+ remove(id: string): Promise<void>;
38
+ clearAll(): Promise<void>;
39
+ }
40
+
41
+ declare class AxiomEngine {
42
+ private config;
43
+ private storage;
44
+ private syncManager;
45
+ /**
46
+ * Initializes the Axiom engine with your specific rules and storage.
47
+ */
48
+ create(config: AxiomConfig, storageAdapter?: AxiomStorageAdapter): void;
49
+ forceSync(): Promise<void>;
50
+ /**
51
+ * Generates a unique ID for queued requests.
52
+ */
53
+ private generateId;
54
+ /**
55
+ * The core POST method.
56
+ */
57
+ post<T>(url: string, data?: any, options?: {
58
+ priority?: RequestPriority;
59
+ }): Promise<{
60
+ data?: T;
61
+ status: number;
62
+ isQueued: boolean;
63
+ }>;
64
+ /** The core GET method.
65
+ * Note: GET requests typically don't have a body, but we still want to queue them if offline.
66
+ */
67
+ get<T>(url: string, options?: {
68
+ priority?: RequestPriority;
69
+ }): Promise<{
70
+ data?: T;
71
+ status: number;
72
+ isQueued: boolean;
73
+ }>;
74
+ /**
75
+ * Internal logic to fire the request or catch the network drop.
76
+ */
77
+ private attemptFetch;
78
+ /**
79
+ * Saves the request to whatever storage adapter was provided on startup.
80
+ */
81
+ private enqueueRequest;
82
+ }
83
+ declare const axiom: AxiomEngine;
84
+
85
+ declare const AxiomProvider: React.FC<{
86
+ config: AxiomConfig;
87
+ storageAdapter?: AxiomStorageAdapter;
88
+ /** * A function that takes a callback, calls it whenever network state changes,
89
+ * and returns an unsubscribe function.
90
+ */
91
+ networkListener: (callback: (isOnline: boolean) => void) => any;
92
+ children: React.ReactNode;
93
+ }>;
94
+
95
+ declare function useAxiomQueue(): {
96
+ /** Boolean indicating if the device currently has an active connection */
97
+ isOnline: boolean;
98
+ /** Manually trigger the background sync manager */
99
+ forceSync: () => Promise<void>;
100
+ };
101
+
102
+ export { type AxiomConfig, AxiomEngine, AxiomProvider, type AxiomStorageAdapter, MemoryStorageAdapter, type QueuedRequest, type RequestPriority, axiom, useAxiomQueue };
package/dist/index.js ADDED
@@ -0,0 +1,269 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AxiomEngine: () => AxiomEngine,
24
+ AxiomProvider: () => AxiomProvider,
25
+ MemoryStorageAdapter: () => MemoryStorageAdapter,
26
+ axiom: () => axiom,
27
+ useAxiomQueue: () => useAxiomQueue
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/adapters/memory.ts
32
+ var MemoryStorageAdapter = class {
33
+ constructor() {
34
+ this.queue = /* @__PURE__ */ new Map();
35
+ }
36
+ async save(request) {
37
+ this.queue.set(request.id, request);
38
+ }
39
+ async getAll() {
40
+ return Array.from(this.queue.values()).sort((a, b) => a.timestamp - b.timestamp);
41
+ }
42
+ async remove(id) {
43
+ this.queue.delete(id);
44
+ }
45
+ async clearAll() {
46
+ this.queue.clear();
47
+ }
48
+ };
49
+
50
+ // src/engine/sync.ts
51
+ var SyncManager = class {
52
+ constructor(storage, config) {
53
+ this.storage = storage;
54
+ this.config = config;
55
+ this.isSyncing = false;
56
+ }
57
+ /**
58
+ * The master trigger. Call this when the OS reports network is back online.
59
+ */
60
+ async flushQueue() {
61
+ if (this.isSyncing) return;
62
+ this.isSyncing = true;
63
+ try {
64
+ const pending = await this.storage.getAll();
65
+ if (pending.length === 0) {
66
+ this.isSyncing = false;
67
+ return;
68
+ }
69
+ console.log(`[Axiom] Network restored. Syncing ${pending.length} queued requests...`);
70
+ for (const request of pending) {
71
+ await this.processRequest(request);
72
+ }
73
+ } finally {
74
+ this.isSyncing = false;
75
+ }
76
+ }
77
+ /**
78
+ * Attempts to execute a single saved request.
79
+ */
80
+ async processRequest(request) {
81
+ let reqToSync = request;
82
+ if (this.config.onBeforeSync) {
83
+ try {
84
+ reqToSync = await this.config.onBeforeSync(request);
85
+ } catch (error) {
86
+ console.error(`[Axiom] onBeforeSync failed for ${request.id}. Skipping.`);
87
+ return;
88
+ }
89
+ }
90
+ try {
91
+ const response = await fetch(reqToSync.url, {
92
+ method: reqToSync.method,
93
+ headers: reqToSync.headers,
94
+ body: reqToSync.body
95
+ });
96
+ if (response.ok || response.status >= 400 && response.status < 500) {
97
+ await this.storage.remove(reqToSync.id);
98
+ console.log(`[Axiom] Request ${reqToSync.id} synced successfully.`);
99
+ } else {
100
+ await this.handleFailure(reqToSync);
101
+ }
102
+ } catch (error) {
103
+ await this.handleFailure(reqToSync);
104
+ }
105
+ }
106
+ /**
107
+ * MITIGATION 2: The Dead Letter Queue logic
108
+ */
109
+ async handleFailure(request) {
110
+ request.retryCount += 1;
111
+ const maxRetries = this.config.maxRetries || 3;
112
+ if (request.retryCount >= maxRetries) {
113
+ console.warn(`[Axiom] Request ${request.id} failed ${maxRetries} times. Moving to Dead Letter.`);
114
+ await this.storage.remove(request.id);
115
+ if (this.config.onDeadLetter) {
116
+ this.config.onDeadLetter(request, new Error("Max retries exceeded"));
117
+ }
118
+ } else {
119
+ await this.storage.save(request);
120
+ }
121
+ }
122
+ };
123
+
124
+ // src/engine/fetcher.ts
125
+ var AxiomEngine = class {
126
+ constructor() {
127
+ this.config = {};
128
+ this.storage = new MemoryStorageAdapter();
129
+ }
130
+ /**
131
+ * Initializes the Axiom engine with your specific rules and storage.
132
+ */
133
+ create(config, storageAdapter) {
134
+ this.config = config;
135
+ if (storageAdapter) {
136
+ this.storage = storageAdapter;
137
+ }
138
+ this.syncManager = new SyncManager(this.storage, this.config);
139
+ }
140
+ async forceSync() {
141
+ if (!this.syncManager) {
142
+ console.error("[Axiom] Engine not initialized. Call axiom.create() first.");
143
+ return;
144
+ }
145
+ await this.syncManager.flushQueue();
146
+ }
147
+ /**
148
+ * Generates a unique ID for queued requests.
149
+ */
150
+ generateId() {
151
+ return Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
152
+ }
153
+ /**
154
+ * The core POST method.
155
+ */
156
+ async post(url, data, options) {
157
+ const fullUrl = this.config.baseURL ? `${this.config.baseURL}${url}` : url;
158
+ const request = {
159
+ id: this.generateId(),
160
+ timestamp: Date.now(),
161
+ url: fullUrl,
162
+ method: "POST",
163
+ headers: {
164
+ "Content-Type": "application/json",
165
+ ...this.config.defaultHeaders || {}
166
+ },
167
+ body: data ? JSON.stringify(data) : void 0,
168
+ priority: options?.priority || "urgent",
169
+ retryCount: 0
170
+ };
171
+ return this.attemptFetch(request);
172
+ }
173
+ /** The core GET method.
174
+ * Note: GET requests typically don't have a body, but we still want to queue them if offline.
175
+ */
176
+ async get(url, options) {
177
+ const fullUrl = this.config.baseURL ? `${this.config.baseURL}${url}` : url;
178
+ const request = {
179
+ id: this.generateId(),
180
+ timestamp: Date.now(),
181
+ url: fullUrl,
182
+ method: "GET",
183
+ headers: {
184
+ ...this.config.defaultHeaders || {}
185
+ },
186
+ priority: options?.priority || "urgent",
187
+ retryCount: 0
188
+ };
189
+ return this.attemptFetch(request);
190
+ }
191
+ /**
192
+ * Internal logic to fire the request or catch the network drop.
193
+ */
194
+ async attemptFetch(request) {
195
+ try {
196
+ const response = await fetch(request.url, {
197
+ method: request.method,
198
+ headers: request.headers,
199
+ body: request.body
200
+ });
201
+ if (response.ok) {
202
+ const responseData = await response.json().catch(() => null);
203
+ return { data: responseData, status: response.status, isQueued: false };
204
+ }
205
+ if (response.status >= 500) {
206
+ throw new Error("Server Error");
207
+ }
208
+ return { status: response.status, isQueued: false };
209
+ } catch (error) {
210
+ await this.enqueueRequest(request);
211
+ return { status: 202, isQueued: true };
212
+ }
213
+ }
214
+ /**
215
+ * Saves the request to whatever storage adapter was provided on startup.
216
+ */
217
+ async enqueueRequest(request) {
218
+ console.warn(`[Axiom] Network unreachable. Queuing request ${request.id}`);
219
+ await this.storage.save(request);
220
+ }
221
+ };
222
+ var axiom = new AxiomEngine();
223
+
224
+ // src/react/AxiomProvider.tsx
225
+ var import_react = require("react");
226
+ var import_jsx_runtime = require("react/jsx-runtime");
227
+ var AxiomContext = (0, import_react.createContext)({
228
+ isOnline: true,
229
+ forceSync: async () => {
230
+ }
231
+ });
232
+ var AxiomProvider = ({ config, storageAdapter, networkListener, children }) => {
233
+ const [isOnline, setIsOnline] = (0, import_react.useState)(true);
234
+ (0, import_react.useEffect)(() => {
235
+ axiom.create(config, storageAdapter);
236
+ const unsubscribe = networkListener((onlineStatus) => {
237
+ setIsOnline(onlineStatus);
238
+ if (onlineStatus) {
239
+ axiom.forceSync();
240
+ }
241
+ });
242
+ return () => {
243
+ if (typeof unsubscribe === "function") {
244
+ unsubscribe();
245
+ }
246
+ };
247
+ }, [config, storageAdapter, networkListener]);
248
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AxiomContext.Provider, { value: { isOnline, forceSync: () => axiom.forceSync() }, children });
249
+ };
250
+ var useAxiomContext = () => (0, import_react.useContext)(AxiomContext);
251
+
252
+ // src/react/useAxiomQueue.ts
253
+ function useAxiomQueue() {
254
+ const { isOnline, forceSync } = useAxiomContext();
255
+ return {
256
+ /** Boolean indicating if the device currently has an active connection */
257
+ isOnline,
258
+ /** Manually trigger the background sync manager */
259
+ forceSync
260
+ };
261
+ }
262
+ // Annotate the CommonJS export names for ESM import in node:
263
+ 0 && (module.exports = {
264
+ AxiomEngine,
265
+ AxiomProvider,
266
+ MemoryStorageAdapter,
267
+ axiom,
268
+ useAxiomQueue
269
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,238 @@
1
+ // src/adapters/memory.ts
2
+ var MemoryStorageAdapter = class {
3
+ constructor() {
4
+ this.queue = /* @__PURE__ */ new Map();
5
+ }
6
+ async save(request) {
7
+ this.queue.set(request.id, request);
8
+ }
9
+ async getAll() {
10
+ return Array.from(this.queue.values()).sort((a, b) => a.timestamp - b.timestamp);
11
+ }
12
+ async remove(id) {
13
+ this.queue.delete(id);
14
+ }
15
+ async clearAll() {
16
+ this.queue.clear();
17
+ }
18
+ };
19
+
20
+ // src/engine/sync.ts
21
+ var SyncManager = class {
22
+ constructor(storage, config) {
23
+ this.storage = storage;
24
+ this.config = config;
25
+ this.isSyncing = false;
26
+ }
27
+ /**
28
+ * The master trigger. Call this when the OS reports network is back online.
29
+ */
30
+ async flushQueue() {
31
+ if (this.isSyncing) return;
32
+ this.isSyncing = true;
33
+ try {
34
+ const pending = await this.storage.getAll();
35
+ if (pending.length === 0) {
36
+ this.isSyncing = false;
37
+ return;
38
+ }
39
+ console.log(`[Axiom] Network restored. Syncing ${pending.length} queued requests...`);
40
+ for (const request of pending) {
41
+ await this.processRequest(request);
42
+ }
43
+ } finally {
44
+ this.isSyncing = false;
45
+ }
46
+ }
47
+ /**
48
+ * Attempts to execute a single saved request.
49
+ */
50
+ async processRequest(request) {
51
+ let reqToSync = request;
52
+ if (this.config.onBeforeSync) {
53
+ try {
54
+ reqToSync = await this.config.onBeforeSync(request);
55
+ } catch (error) {
56
+ console.error(`[Axiom] onBeforeSync failed for ${request.id}. Skipping.`);
57
+ return;
58
+ }
59
+ }
60
+ try {
61
+ const response = await fetch(reqToSync.url, {
62
+ method: reqToSync.method,
63
+ headers: reqToSync.headers,
64
+ body: reqToSync.body
65
+ });
66
+ if (response.ok || response.status >= 400 && response.status < 500) {
67
+ await this.storage.remove(reqToSync.id);
68
+ console.log(`[Axiom] Request ${reqToSync.id} synced successfully.`);
69
+ } else {
70
+ await this.handleFailure(reqToSync);
71
+ }
72
+ } catch (error) {
73
+ await this.handleFailure(reqToSync);
74
+ }
75
+ }
76
+ /**
77
+ * MITIGATION 2: The Dead Letter Queue logic
78
+ */
79
+ async handleFailure(request) {
80
+ request.retryCount += 1;
81
+ const maxRetries = this.config.maxRetries || 3;
82
+ if (request.retryCount >= maxRetries) {
83
+ console.warn(`[Axiom] Request ${request.id} failed ${maxRetries} times. Moving to Dead Letter.`);
84
+ await this.storage.remove(request.id);
85
+ if (this.config.onDeadLetter) {
86
+ this.config.onDeadLetter(request, new Error("Max retries exceeded"));
87
+ }
88
+ } else {
89
+ await this.storage.save(request);
90
+ }
91
+ }
92
+ };
93
+
94
+ // src/engine/fetcher.ts
95
+ var AxiomEngine = class {
96
+ constructor() {
97
+ this.config = {};
98
+ this.storage = new MemoryStorageAdapter();
99
+ }
100
+ /**
101
+ * Initializes the Axiom engine with your specific rules and storage.
102
+ */
103
+ create(config, storageAdapter) {
104
+ this.config = config;
105
+ if (storageAdapter) {
106
+ this.storage = storageAdapter;
107
+ }
108
+ this.syncManager = new SyncManager(this.storage, this.config);
109
+ }
110
+ async forceSync() {
111
+ if (!this.syncManager) {
112
+ console.error("[Axiom] Engine not initialized. Call axiom.create() first.");
113
+ return;
114
+ }
115
+ await this.syncManager.flushQueue();
116
+ }
117
+ /**
118
+ * Generates a unique ID for queued requests.
119
+ */
120
+ generateId() {
121
+ return Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
122
+ }
123
+ /**
124
+ * The core POST method.
125
+ */
126
+ async post(url, data, options) {
127
+ const fullUrl = this.config.baseURL ? `${this.config.baseURL}${url}` : url;
128
+ const request = {
129
+ id: this.generateId(),
130
+ timestamp: Date.now(),
131
+ url: fullUrl,
132
+ method: "POST",
133
+ headers: {
134
+ "Content-Type": "application/json",
135
+ ...this.config.defaultHeaders || {}
136
+ },
137
+ body: data ? JSON.stringify(data) : void 0,
138
+ priority: options?.priority || "urgent",
139
+ retryCount: 0
140
+ };
141
+ return this.attemptFetch(request);
142
+ }
143
+ /** The core GET method.
144
+ * Note: GET requests typically don't have a body, but we still want to queue them if offline.
145
+ */
146
+ async get(url, options) {
147
+ const fullUrl = this.config.baseURL ? `${this.config.baseURL}${url}` : url;
148
+ const request = {
149
+ id: this.generateId(),
150
+ timestamp: Date.now(),
151
+ url: fullUrl,
152
+ method: "GET",
153
+ headers: {
154
+ ...this.config.defaultHeaders || {}
155
+ },
156
+ priority: options?.priority || "urgent",
157
+ retryCount: 0
158
+ };
159
+ return this.attemptFetch(request);
160
+ }
161
+ /**
162
+ * Internal logic to fire the request or catch the network drop.
163
+ */
164
+ async attemptFetch(request) {
165
+ try {
166
+ const response = await fetch(request.url, {
167
+ method: request.method,
168
+ headers: request.headers,
169
+ body: request.body
170
+ });
171
+ if (response.ok) {
172
+ const responseData = await response.json().catch(() => null);
173
+ return { data: responseData, status: response.status, isQueued: false };
174
+ }
175
+ if (response.status >= 500) {
176
+ throw new Error("Server Error");
177
+ }
178
+ return { status: response.status, isQueued: false };
179
+ } catch (error) {
180
+ await this.enqueueRequest(request);
181
+ return { status: 202, isQueued: true };
182
+ }
183
+ }
184
+ /**
185
+ * Saves the request to whatever storage adapter was provided on startup.
186
+ */
187
+ async enqueueRequest(request) {
188
+ console.warn(`[Axiom] Network unreachable. Queuing request ${request.id}`);
189
+ await this.storage.save(request);
190
+ }
191
+ };
192
+ var axiom = new AxiomEngine();
193
+
194
+ // src/react/AxiomProvider.tsx
195
+ import { createContext, useContext, useEffect, useState } from "react";
196
+ import { jsx } from "react/jsx-runtime";
197
+ var AxiomContext = createContext({
198
+ isOnline: true,
199
+ forceSync: async () => {
200
+ }
201
+ });
202
+ var AxiomProvider = ({ config, storageAdapter, networkListener, children }) => {
203
+ const [isOnline, setIsOnline] = useState(true);
204
+ useEffect(() => {
205
+ axiom.create(config, storageAdapter);
206
+ const unsubscribe = networkListener((onlineStatus) => {
207
+ setIsOnline(onlineStatus);
208
+ if (onlineStatus) {
209
+ axiom.forceSync();
210
+ }
211
+ });
212
+ return () => {
213
+ if (typeof unsubscribe === "function") {
214
+ unsubscribe();
215
+ }
216
+ };
217
+ }, [config, storageAdapter, networkListener]);
218
+ return /* @__PURE__ */ jsx(AxiomContext.Provider, { value: { isOnline, forceSync: () => axiom.forceSync() }, children });
219
+ };
220
+ var useAxiomContext = () => useContext(AxiomContext);
221
+
222
+ // src/react/useAxiomQueue.ts
223
+ function useAxiomQueue() {
224
+ const { isOnline, forceSync } = useAxiomContext();
225
+ return {
226
+ /** Boolean indicating if the device currently has an active connection */
227
+ isOnline,
228
+ /** Manually trigger the background sync manager */
229
+ forceSync
230
+ };
231
+ }
232
+ export {
233
+ AxiomEngine,
234
+ AxiomProvider,
235
+ MemoryStorageAdapter,
236
+ axiom,
237
+ useAxiomQueue
238
+ };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@jayethian/axiom",
3
+ "version": "0.1.0",
4
+ "maintainers": [
5
+ "Jayetheus"
6
+ ],
7
+ "description": "A resilient, offline-first fetch wrapper",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "main": "./dist/index.js",
12
+ "module": "./dist/index.mjs",
13
+ "types": "./dist/index.d.ts",
14
+ "scripts": {
15
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
16
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
17
+ "test": "vitest run",
18
+ "typecheck": "tsc --noEmit"
19
+ },
20
+ "author": "Jayetheus",
21
+ "license": "MIT",
22
+ "devDependencies": {
23
+ "@types/react": "^19.2.14",
24
+ "react": "^19.2.6",
25
+ "tsup": "^8.5.1",
26
+ "typescript": "^6.0.3",
27
+ "vitest": "^4.1.6"
28
+ },
29
+ "peerDependencies": {
30
+ "react": ">=17.0.0"
31
+ },
32
+ "packageManager": "yarn@4.12.0"
33
+ }
@@ -0,0 +1,15 @@
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
+ }
@@ -0,0 +1,22 @@
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
+ }
@@ -0,0 +1,131 @@
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();
@@ -0,0 +1,96 @@
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
+ */
15
+ public async flushQueue(): Promise<void> {
16
+ // Prevent overlapping syncs if the network toggles rapidly
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
+ this.isSyncing = false;
25
+ return;
26
+ }
27
+
28
+ console.log(`[Axiom] Network restored. Syncing ${pending.length} queued requests...`);
29
+
30
+ // We process sequentially to maintain the order of user actions
31
+ for (const request of pending) {
32
+ await this.processRequest(request);
33
+ }
34
+
35
+ } finally {
36
+ this.isSyncing = false;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Attempts to execute a single saved request.
42
+ */
43
+ private async processRequest(request: QueuedRequest): Promise<void> {
44
+ let reqToSync = request;
45
+
46
+ // MITIGATION 1: Just-in-Time Headers (Refresh Auth Tokens)
47
+ if (this.config.onBeforeSync) {
48
+ try {
49
+ reqToSync = await this.config.onBeforeSync(request);
50
+ } catch (error) {
51
+ console.error(`[Axiom] onBeforeSync failed for ${request.id}. Skipping.`);
52
+ return;
53
+ }
54
+ }
55
+
56
+ try {
57
+ const response = await fetch(reqToSync.url, {
58
+ method: reqToSync.method,
59
+ headers: reqToSync.headers,
60
+ body: reqToSync.body
61
+ });
62
+
63
+ // If it's a success OR a permanent 400 error (like bad data), remove it.
64
+ if (response.ok || (response.status >= 400 && response.status < 500)) {
65
+ await this.storage.remove(reqToSync.id);
66
+ console.log(`[Axiom] Request ${reqToSync.id} synced successfully.`);
67
+ } else {
68
+ // It's a 500 Server Error. Treat as a failure and retry later.
69
+ await this.handleFailure(reqToSync);
70
+ }
71
+ } catch (error) {
72
+ // Network dropped again mid-sync.
73
+ await this.handleFailure(reqToSync);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * MITIGATION 2: The Dead Letter Queue logic
79
+ */
80
+ private async handleFailure(request: QueuedRequest): Promise<void> {
81
+ request.retryCount += 1;
82
+ const maxRetries = this.config.maxRetries || 3;
83
+
84
+ if (request.retryCount >= maxRetries) {
85
+ console.warn(`[Axiom] Request ${request.id} failed ${maxRetries} times. Moving to Dead Letter.`);
86
+ await this.storage.remove(request.id);
87
+
88
+ if (this.config.onDeadLetter) {
89
+ this.config.onDeadLetter(request, new Error('Max retries exceeded'));
90
+ }
91
+ } else {
92
+ // Save the updated retry count back to storage
93
+ await this.storage.save(request);
94
+ }
95
+ }
96
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
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';
@@ -0,0 +1,57 @@
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);
@@ -0,0 +1,12 @@
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 ADDED
@@ -0,0 +1,20 @@
1
+ export type RequestPriority = 'urgent' | 'background';
2
+
3
+ export interface QueuedRequest {
4
+ id: string;
5
+ timestamp: number;
6
+ url: string;
7
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
8
+ headers: Record<string, string>;
9
+ body?: string;
10
+ priority: RequestPriority;
11
+ retryCount: number;
12
+ }
13
+
14
+ export interface AxiomConfig {
15
+ baseURL?: string;
16
+ defaultHeaders?: Record<string, string>;
17
+ maxRetries?: number;
18
+ onBeforeSync?: (request: QueuedRequest) => Promise<QueuedRequest>;
19
+ onDeadLetter?: (request: QueuedRequest, error: Error) => void;
20
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
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
+ }