@jammysunshine/astrology-api-client 1.0.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/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@jammysunshine/astrology-api-client",
3
+ "version": "1.0.0",
4
+ "description": "Unified API Client for Astrology Services",
5
+ "main": "src/index.js",
6
+ "scripts": {
7
+ "test": "node ../../node_modules/.bin/jest"
8
+ },
9
+ "dependencies": {
10
+ "axios": "^1.6.0",
11
+ "uuid": "^9.0.1"
12
+ },
13
+ "engines": {
14
+ "node": ">=18.0.0"
15
+ }
16
+ }
@@ -0,0 +1,254 @@
1
+ const { v4: uuid } = require('uuid');
2
+ const axios = require('axios');
3
+ const http = require('http');
4
+ const https = require('https');
5
+ const retry = require('./utils/retry');
6
+ const breaker = new (require('./utils/CircuitBreaker'))();
7
+ const monitor = require('./utils/NetworkMonitor');
8
+ const detective = require('./utils/ResponseDetective');
9
+ const diagnostics = require('./utils/DiagnosticsTracker');
10
+ const ContextStore = require('./utils/ContextStore');
11
+ const SDK_CONFIG = require('./config');
12
+
13
+ class AstrologyApiClient {
14
+ constructor(config = {}) {
15
+ this.config = {
16
+ baseUrl: config.baseUrl || SDK_CONFIG.DEFAULT_BASE_URL,
17
+ apiKey: config.apiKey || '', // SDK Sec 5: SECURITY: Ensure apiKey is managed via secure environment variables
18
+ platform: config.platform || 'api',
19
+ clientType: config.clientType || 'api-client',
20
+ clientVersion: config.clientVersion || '1.0.0',
21
+ apiVersion: config.apiVersion || 'v1', // API versioning support
22
+ timeout: config.timeout || SDK_CONFIG.DEFAULT_TIMEOUT,
23
+ lockTtl: SDK_CONFIG.LOCK_TTL,
24
+ services: config.services || {},
25
+ onMetrics: config.onMetrics || (() => {}),
26
+ ...config
27
+ };
28
+
29
+ const agentOptions = SDK_CONFIG.AGENT_OPTIONS;
30
+ this.axios = axios.create({
31
+ timeout: this.config.timeout,
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ 'Authorization': `Bearer ${this.config.apiKey}`, // SDK Sec 5
35
+ 'X-API-Version': this.config.apiVersion // API versioning header
36
+ },
37
+ httpAgent: new http.Agent(agentOptions),
38
+ httpsAgent: new https.Agent(agentOptions)
39
+ });
40
+
41
+ this.breaker = breaker; // Expose for BotEngine (Design Sec 3.7)
42
+ this.monitor = monitor;
43
+ this.diagnostics = diagnostics;
44
+ this.contextStore = new ContextStore(config.memory);
45
+ this.requestBatcher = new (require('./utils/RequestBatcher'))(this);
46
+ this.interceptors = new (require('./utils/Interceptors'))();
47
+ this.shimMap = SDK_CONFIG.COMPATIBILITY_MAP;
48
+
49
+ return new Proxy(this, {
50
+ get: (target, key) => {
51
+ if (key in target) return target[key];
52
+ if (/^v\d+$/.test(key)) return new Proxy({}, { get: (_, s) => target._createServiceProxy(s, key) });
53
+
54
+ // Only proxy service names that are expected
55
+ if (typeof key === 'string' && !key.startsWith('_')) {
56
+ return target._createServiceProxy(key, 'v1');
57
+ }
58
+
59
+ return undefined;
60
+ }
61
+ });
62
+ }
63
+
64
+ async batch(calls) {
65
+ return this.requestBatcher.batch(calls);
66
+ }
67
+
68
+ _createServiceProxy(service, version) {
69
+ return new Proxy({}, {
70
+ get: (_, method) => async (params = {}) => {
71
+ const shimmed = this._resolveShim(service, method);
72
+ return this._executeRequest(shimmed.service, shimmed.method, params, version);
73
+ }
74
+ });
75
+ }
76
+
77
+ _resolveShim(s, m) {
78
+ const mapped = this.shimMap[`${s}.${m}`];
79
+ return mapped ? { service: mapped.split('.')[0], method: mapped.split('.')[1] } : { service: s, method: m };
80
+ }
81
+
82
+ asUser(platformUserId) {
83
+ const client = this;
84
+ const ensureIdentity = async () => {
85
+ let ctx = await client.contextStore.get(platformUserId);
86
+ if (!ctx || !ctx.unifiedUserId) {
87
+ const res = await client._executeRequest('userMapping', 'mapPlatformToUnified', { platform: client.config.platform, platformUserId, initialProfileData: {} });
88
+ ctx = { unifiedUserId: res.unifiedUserId, sessionId: res.sessionId };
89
+ await client.contextStore.set(platformUserId, ctx);
90
+ }
91
+ return ctx;
92
+ };
93
+
94
+ return {
95
+ observability: {
96
+ incrementCounter: (name, tags) => client._executeRequest('observability', 'recordMetric', { type: 'counter', name, tags: { ...tags, platform: client.config.platform, platformUserId } }),
97
+ recordHistogram: (name, value, tags) => client._executeRequest('observability', 'recordMetric', { type: 'histogram', name, value, tags: { ...tags, platform: client.config.platform, platformUserId } })
98
+ },
99
+ // Media lifecycle management (Design Sec 16.2)
100
+ media: {
101
+ getMediaId: async (hash) => client._executeRequest('mediaManager', 'getMediaId', { hash }),
102
+ recordMediaId: async (hash, mediaId, ttl) => client._executeRequest('mediaManager', 'recordMediaId', { hash, mediaId, ttl })
103
+ },
104
+ acquireSessionLock: async () => retry(async () => client._executeRequest('sessionManager', 'acquireSessionLock', { unifiedUserId: (await ensureIdentity()).unifiedUserId, ttl: client.config.lockTtl }), { maxAttempts: 5 }),
105
+ getSession: async () => {
106
+ const ctx = await ensureIdentity();
107
+ const res = await client._executeRequest('sessionManager', 'getSessionState', { unifiedUserId: ctx.unifiedUserId });
108
+ ctx.sessionId = res.sessionId;
109
+ await client.contextStore.set(platformUserId, ctx);
110
+ return res;
111
+ },
112
+ save: async (state) => {
113
+ const ctx = await ensureIdentity();
114
+ const res = await client._executeRequest('sessionManager', 'updateSessionState', { unifiedUserId: ctx.unifiedUserId, sessionId: ctx.sessionId, partialState: state });
115
+ await client._executeRequest('sessionManager', 'releaseSessionLock', { unifiedUserId: ctx.unifiedUserId });
116
+ return res;
117
+ },
118
+ call: async (s, m, p) => {
119
+ const ctx = await ensureIdentity();
120
+ try {
121
+ return await client._executeRequest(s, m, { ...p, unifiedUserId: ctx.unifiedUserId, platform: client.config.platform, platformUserId });
122
+ } catch (error) {
123
+ if (error.status === 401) {
124
+ await client.contextStore.set(platformUserId, { unifiedUserId: null });
125
+ const newCtx = await ensureIdentity();
126
+ return await client._executeRequest(s, m, { ...p, unifiedUserId: newCtx.unifiedUserId, platform: client.config.platform, platformUserId });
127
+ }
128
+ throw error;
129
+ }
130
+ }
131
+ };
132
+ }
133
+
134
+ async _executeRequest(service, method, params = {}, version = 'v1') {
135
+ const startTime = Date.now();
136
+ // SDK Sec 9.A: Intelligent Request Caching
137
+ const cacheKey = `req:${service}:${method}:${JSON.stringify(params)}`;
138
+ const cached = await this.contextStore.get(cacheKey);
139
+ if (cached && (service === 'birthchart' || service === 'dasha')) {
140
+ return cached;
141
+ }
142
+
143
+ const t0 = Date.now(); // Prep end
144
+ const requestId = uuid();
145
+
146
+ // Hard Request Ceiling enforcement
147
+ const baseTimeout = this.monitor.status === 'flaky' ? SDK_CONFIG.FLAKY_TIMEOUT : this.config.timeout;
148
+ const timeout = Math.min(baseTimeout, SDK_CONFIG.MAX_TIMEOUT_CEILING);
149
+
150
+ const serviceBase = this.config.services[service] || this.config.baseUrl;
151
+
152
+ this.interceptors.onRequest({ requestId, service, method, params });
153
+
154
+ // Define failure criteria for Circuit Breaker: 5xx errors or Timeouts/Network issues
155
+ const isCircuitBreakerFailure = (error) => {
156
+ if (!error.status) return true; // Network/Timeout/Unknown
157
+ return error.status >= 500; // Server Error
158
+ };
159
+
160
+ return this.breaker.execute(() => retry(async () => {
161
+ let t1 = Date.now();
162
+ let responseData = null;
163
+ let responseStatus = null;
164
+ let responseContentType = null;
165
+
166
+ try {
167
+ const response = await this.axios.post(`${serviceBase}/api/${version}/${service}/${method}`,
168
+ { requestId, timestamp: new Date().toISOString(), ...params }, { timeout, responseType: 'arraybuffer' }
169
+ );
170
+ responseData = response.data;
171
+ responseStatus = response.status;
172
+ responseContentType = response.headers['content-type'];
173
+
174
+ const t2 = Date.now();
175
+ const duration = t2 - t1;
176
+ this.monitor.record(duration, true);
177
+ this.diagnostics.recordRequest(JSON.stringify(params).length, responseData.length);
178
+
179
+ let resultData;
180
+ try {
181
+ const format = detective.detect({ data: responseData, headers: { 'content-type': responseContentType } });
182
+
183
+ if (format === 'JSON') {
184
+ const json = JSON.parse(responseData.toString());
185
+ if (json.status === 'error') throw this._wrapError(json.error, requestId);
186
+ resultData = json.data;
187
+ // SDK Sec 1: Smart Unwrapping
188
+ if (resultData && typeof resultData === 'object') {
189
+ Object.defineProperty(resultData, 'metadata', { value: json.metadata || { requestId }, enumerable: false });
190
+ }
191
+ } else if (format === 'TEXT') {
192
+ resultData = responseData.toString();
193
+ } else {
194
+ resultData = responseData;
195
+ }
196
+ } catch (parseError) {
197
+ if (parseError instanceof ApiClientError) throw parseError;
198
+ const { ApiParseError } = require('./utils/ApiClientError');
199
+ throw new ApiParseError(`Failed to parse ${responseContentType} response: ${parseError.message}`, {
200
+ requestId,
201
+ rawData: responseData ? responseData.toString().substring(0, 1000) : null
202
+ });
203
+ }
204
+
205
+ this.interceptors.onResponse({ status: responseStatus, headers: { 'content-type': responseContentType } }, resultData);
206
+
207
+ // Cache successful immutable requests
208
+ if (service === 'birthchart' || service === 'dasha') {
209
+ await this.contextStore.set(cacheKey, resultData);
210
+ }
211
+
212
+ const t3 = Date.now();
213
+ this.config.onMetrics({
214
+ requestId,
215
+ prep_time: t0 - startTime, // SDK Sec 9.D
216
+ total_duration: t3 - startTime,
217
+ latency_ttfb: t2 - t1,
218
+ transfer_time: t3 - t2
219
+ });
220
+
221
+ return resultData;
222
+ } catch (error) {
223
+ this.monitor.record(Date.now() - (t1 || t0), false);
224
+ this.diagnostics.trackError(error.name || error.code || 'UNKNOWN', requestId);
225
+ throw this.interceptors.onError(error);
226
+ }
227
+ }, { shouldRetry: (err) => SDK_CONFIG.RETRYABLE_STATUSES.includes(err.status) }), isCircuitBreakerFailure);
228
+ }
229
+
230
+ _wrapError(apiError, requestId) {
231
+ const errorClasses = require('./utils/ApiClientError');
232
+ const { code, message, details, statusCode } = apiError;
233
+
234
+ let ErrorClass = errorClasses.ApiClientError;
235
+
236
+ switch (code) {
237
+ case 'VALIDATION_ERROR': ErrorClass = errorClasses.ApiValidationError; break;
238
+ case 'INVALID_BIRTH_DATA': ErrorClass = errorClasses.ApiInvalidBirthDataError; break;
239
+ case 'RATE_LIMIT_EXCEEDED': ErrorClass = errorClasses.ApiRateLimitError; break;
240
+ case 'EPHEMERIS_UNAVAILABLE': ErrorClass = errorClasses.ApiEphemerisError; break;
241
+ case 'AI_SERVICE_ERROR': ErrorClass = errorClasses.ApiAIServiceError; break;
242
+ case 'TIMEOUT_ERROR': ErrorClass = errorClasses.ApiTimeoutError; break;
243
+ case 'CALCULATION_ERROR': ErrorClass = errorClasses.ApiCalculationError; break;
244
+ case 'DATABASE_ERROR': ErrorClass = errorClasses.ApiDatabaseError; break;
245
+ case 'INTERNAL_ERROR': ErrorClass = errorClasses.ApiInternalError; break;
246
+ }
247
+
248
+ const err = new ErrorClass(message, { ...details, requestId });
249
+ err.status = statusCode;
250
+ return err;
251
+ }
252
+ }
253
+
254
+ module.exports = AstrologyApiClient;
package/src/config.js ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Global Configuration for Astrology API Client (SDK Sec 2)
3
+ */
4
+ module.exports = {
5
+ DEFAULT_BASE_URL: process.env.ASTROLOGY_API_BASE_URL || 'http://localhost:10000',
6
+ DEFAULT_TIMEOUT: Math.max(1000, Math.min(parseInt(process.env.ASTROLOGY_API_TIMEOUT, 10) || 15000, 60000)),
7
+ MAX_TIMEOUT_CEILING: 60000,
8
+ FLAKY_TIMEOUT: parseInt(process.env.ASTROLOGY_FLAKY_TIMEOUT, 10) || 60000,
9
+ MEMORY_HWM: parseFloat(process.env.ASTROLOGY_MEMORY_HWM) || 0.8, // High Water Mark for Aggressive Mode
10
+ IDLE_TIMEOUT: parseInt(process.env.ASTROLOGY_IDLE_TIMEOUT, 10) || 15 * 60 * 1000, // 15 Minutes
11
+ LOCK_TTL: parseInt(process.env.ASTROLOGY_LOCK_TTL, 10) || 5000,
12
+
13
+ // Connection Pooling (SDK Sec 9.C)
14
+ AGENT_OPTIONS: {
15
+ keepAlive: true,
16
+ maxSockets: parseInt(process.env.ASTROLOGY_MAX_SOCKETS, 10) || 100,
17
+ freeSocketTimeout: parseInt(process.env.ASTROLOGY_FREE_SOCKET_TIMEOUT, 10) || 30000
18
+ },
19
+
20
+ // Compatibility Mapping (Shim Layer - SDK Sec 13.B)
21
+ COMPATIBILITY_MAP: {
22
+ 'dasha.calculateDasha': 'dasha.getDashaPredictiveAnalysis',
23
+ 'birthchart.getSummary': 'birthchart.getBirthChartSummary'
24
+ },
25
+
26
+ RETRYABLE_STATUSES: [408, 500, 502, 503, 504]
27
+ };
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ const AstrologyApiClient = require('./AstrologyApiClient');
2
+ const ApiClientError = require('./utils/ApiClientError');
3
+
4
+ module.exports = {
5
+ AstrologyApiClient,
6
+ ...ApiClientError
7
+ };
@@ -0,0 +1,73 @@
1
+ class ApiClientError extends Error {
2
+ constructor(message, code, details = {}) {
3
+ super(message);
4
+ this.name = this.constructor.name;
5
+ this.code = code || 'API_ERROR';
6
+ this.details = details;
7
+ this.requestId = details.requestId;
8
+ }
9
+ }
10
+
11
+ class ApiValidationError extends ApiClientError {
12
+ constructor(message, details = {}) {
13
+ super(message || 'Validation Failed', 'VALIDATION_ERROR', details);
14
+ this.mappedErrors = this._mapValidationErrors(details.validationErrors);
15
+ }
16
+
17
+ /**
18
+ * Translates raw AJV paths (e.g., "/birthData/time") into clean UI-friendly nested objects (SDK Sec 7.A)
19
+ */
20
+ _mapValidationErrors(errors) {
21
+ if (!Array.isArray(errors)) return {};
22
+ const mapped = {};
23
+
24
+ errors.forEach(err => {
25
+ // Translates "/birthData/time" -> ['birthData', 'time']
26
+ const path = (err.field || err.instancePath || '').split('/').filter(Boolean);
27
+ if (path.length === 0) {
28
+ mapped.general = err.message;
29
+ return;
30
+ }
31
+
32
+ let current = mapped;
33
+ // Navigate/build nested object structure
34
+ for (let i = 0; i < path.length - 1; i++) {
35
+ current[path[i]] = current[path[i]] || {};
36
+ current = current[path[i]];
37
+ }
38
+ // Assign message: { birthData: { time: "Required field missing" } }
39
+ current[path[path.length - 1]] = err.message;
40
+ });
41
+
42
+ return mapped;
43
+ }
44
+ }
45
+
46
+ class ApiInvalidBirthDataError extends ApiClientError { constructor(msg, details) { super(msg || "Invalid birth data", "INVALID_BIRTH_DATA", details); } }
47
+ class ApiRateLimitError extends ApiClientError { constructor(msg, details) { super(msg || "Rate limit exceeded", "RATE_LIMIT_EXCEEDED", details); } }
48
+ class ApiEphemerisError extends ApiClientError { constructor(msg, details) { super(msg || "Ephemeris service unavailable", "EPHEMERIS_UNAVAILABLE", details); } }
49
+ class ApiAIServiceError extends ApiClientError { constructor(msg, details) { super(msg || "AI service unavailable", "AI_SERVICE_ERROR", details); } }
50
+ class ApiTimeoutError extends ApiClientError { constructor(msg, details) { super(msg || "Operation timed out", "TIMEOUT_ERROR", details); } }
51
+ class ApiCalculationError extends ApiClientError { constructor(msg, details) { super(msg || "Calculation failed", "CALCULATION_ERROR", details); } }
52
+ class ApiDatabaseError extends ApiClientError { constructor(msg, details) { super(msg || "Database error", "DATABASE_ERROR", details); } }
53
+ class ApiInternalError extends ApiClientError { constructor(msg, details) { super(msg || "Internal server error", "INTERNAL_ERROR", details); } }
54
+ class ApiParseError extends ApiClientError {
55
+ constructor(msg, details) {
56
+ super(msg || "Failed to parse API response", "PARSE_ERROR", details);
57
+ this.rawData = details.rawData;
58
+ }
59
+ }
60
+
61
+ module.exports = {
62
+ ApiClientError,
63
+ ApiValidationError,
64
+ ApiInvalidBirthDataError,
65
+ ApiRateLimitError,
66
+ ApiEphemerisError,
67
+ ApiAIServiceError,
68
+ ApiTimeoutError,
69
+ ApiCalculationError,
70
+ ApiDatabaseError,
71
+ ApiInternalError,
72
+ ApiParseError
73
+ };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Circuit Breaker Pattern for service resilience
3
+ */
4
+ class CircuitBreaker {
5
+ constructor(options = {}) {
6
+ this.failureThreshold = options.failureThreshold || 5;
7
+ this.resetTimeout = options.resetTimeout || 30000;
8
+ this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
9
+ this.failures = 0;
10
+ this.nextAttempt = 0;
11
+ }
12
+
13
+ async execute(fn, isFailure = () => true) {
14
+ if (this.state === 'OPEN') {
15
+ if (Date.now() > this.nextAttempt) {
16
+ this.state = 'HALF_OPEN';
17
+ } else {
18
+ throw new Error('Circuit Breaker is OPEN');
19
+ }
20
+ }
21
+
22
+ try {
23
+ const result = await fn();
24
+ this.onSuccess();
25
+ return result;
26
+ } catch (error) {
27
+ if (isFailure(error)) {
28
+ this.onFailure();
29
+ }
30
+ throw error;
31
+ }
32
+ }
33
+
34
+ onSuccess() {
35
+ this.failures = 0;
36
+ this.state = 'CLOSED';
37
+ }
38
+
39
+ onFailure() {
40
+ this.failures++;
41
+ if (this.failures >= this.failureThreshold) {
42
+ this.state = 'OPEN';
43
+ this.nextAttempt = Date.now() + this.resetTimeout;
44
+ }
45
+ }
46
+ }
47
+
48
+ module.exports = CircuitBreaker;
@@ -0,0 +1,107 @@
1
+ const EventEmitter = require('events');
2
+
3
+ /**
4
+ * In-memory LRU Cache for immutable astrology data (SDK Sec 8)
5
+ */
6
+ class ContextStore extends EventEmitter {
7
+ constructor(options = {}) {
8
+ super();
9
+ this.maxSize = options.maxSize || 1000;
10
+ this.ttl = options.ttl || 3600 * 1000;
11
+ this.idleTimeout = options.idleTimeout || 15 * 60 * 1000;
12
+
13
+ // SDK Sec 8.A: O(1) LRU using Doubly Linked List
14
+ this.cache = new Map(); // key -> node
15
+ this.head = null; // Most Recent
16
+ this.tail = null; // Least Recent
17
+
18
+ // SDK Sec 8.B: Aggressive Mode
19
+ this._startMemoryMonitor();
20
+ // SDK Sec 8.C: Sweep Cycle
21
+ this._startSweepCycle();
22
+ }
23
+
24
+ async set(key, value) {
25
+ if (this.cache.has(key)) {
26
+ this._remove(this.cache.get(key));
27
+ } else if (this.cache.size >= this.maxSize && this.tail) {
28
+ // Evict Least Recently Used
29
+ this.cache.delete(this.tail.key);
30
+ this._remove(this.tail);
31
+ }
32
+
33
+ const node = { key, value, expiry: Date.now() + this.ttl };
34
+ this._add(node);
35
+ this.cache.set(key, node);
36
+ }
37
+
38
+ async get(key) {
39
+ const node = this.cache.get(key);
40
+ if (!node) return null;
41
+
42
+ if (Date.now() > node.expiry) {
43
+ this.cache.delete(key);
44
+ this._remove(node);
45
+ return null;
46
+ }
47
+
48
+ // Move to front (Most Recently Used)
49
+ this._remove(node);
50
+ this._add(node);
51
+
52
+ return node.value;
53
+ }
54
+
55
+ _add(node) {
56
+ node.next = this.head;
57
+ node.prev = null;
58
+ if (this.head) this.head.prev = node;
59
+ this.head = node;
60
+ if (!this.tail) this.tail = node;
61
+ }
62
+
63
+ _remove(node) {
64
+ if (node.prev) node.prev.next = node.next;
65
+ else this.head = node.next;
66
+
67
+ if (node.next) node.next.prev = node.prev;
68
+ else this.tail = node.prev;
69
+
70
+ // Clean up node references to avoid memory leaks
71
+ node.next = null;
72
+ node.prev = null;
73
+ }
74
+
75
+ _startMemoryMonitor() {
76
+ setInterval(() => {
77
+ const { heapUsed, heapTotal } = process.memoryUsage();
78
+ const usageRatio = heapUsed / heapTotal;
79
+ if (usageRatio > 0.8) {
80
+ this.emit('alert', 'Memory pressure detected. Entering Aggressive Mode.');
81
+ // SDK Sec 8.B: Halve inactivity timeout (minimum 1 minute)
82
+ this.idleTimeout = Math.max(60000, this.idleTimeout / 2);
83
+ this.clear();
84
+ }
85
+ }, 30000);
86
+ }
87
+
88
+ _startSweepCycle() {
89
+ setInterval(() => {
90
+ const now = Date.now();
91
+ for (const [key, node] of this.cache.entries()) {
92
+ if (now > node.expiry) {
93
+ this.cache.delete(key);
94
+ this._remove(node);
95
+ }
96
+ }
97
+ }, 300000);
98
+ }
99
+
100
+ clear() {
101
+ this.cache.clear();
102
+ this.head = null;
103
+ this.tail = null;
104
+ }
105
+ }
106
+
107
+ module.exports = ContextStore;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Aggregates SDK-level errors and diagnostics (SDK Sec 9.E)
3
+ */
4
+ class DiagnosticsTracker {
5
+ constructor() {
6
+ this.stats = {
7
+ errors: {},
8
+ recent_failures: [],
9
+ total_requests: 0,
10
+ total_bytes_sent: 0,
11
+ total_bytes_received: 0
12
+ };
13
+ }
14
+
15
+ recordRequest(sent, received) {
16
+ this.stats.total_requests++;
17
+ this.stats.total_bytes_sent += sent || 0;
18
+ this.stats.total_bytes_received += received || 0;
19
+ }
20
+
21
+ trackError(type, requestId) {
22
+ const category = this._mapToCategory(type);
23
+ this.stats.errors[category] = (this.stats.errors[category] || 0) + 1;
24
+ this.stats.recent_failures.push({ category, requestId, timestamp: Date.now() });
25
+
26
+ if (this.stats.recent_failures.length > 50) this.stats.recent_failures.shift();
27
+ }
28
+
29
+ _mapToCategory(type) {
30
+ if (['ECONNREFUSED', 'ETIMEDOUT', 'ApiNetworkError'].includes(type)) return 'CONNECTION';
31
+ if (['ApiValidationError'].includes(type)) return 'VALIDATION';
32
+ if (['ApiUnauthorizedError'].includes(type)) return 'IDENTITY';
33
+ return 'SERVER';
34
+ }
35
+
36
+ getHealthReport() {
37
+ const NetworkMonitor = require('./NetworkMonitor');
38
+ return {
39
+ uptime: process.uptime(),
40
+ error_distribution: this.stats.errors,
41
+ avg_latency: NetworkMonitor.getAverageLatency(),
42
+ network_status: NetworkMonitor.status,
43
+ usage: {
44
+ total_requests: this.stats.total_requests,
45
+ total_bytes_sent: this.stats.total_bytes_sent,
46
+ total_bytes_received: this.stats.total_bytes_received
47
+ }
48
+ };
49
+ }
50
+ }
51
+
52
+ module.exports = new DiagnosticsTracker();
@@ -0,0 +1,50 @@
1
+ const { getLogger } = require('@astrology/shared/logger');
2
+ const logger = getLogger('api-client-interceptors');
3
+
4
+ /**
5
+ * Global Interceptors for API lifecycle events (SDK Sec 11.B)
6
+ */
7
+ class Interceptors {
8
+ constructor() {
9
+ this.requestHooks = [];
10
+ this.responseHooks = [];
11
+ this.errorHooks = [];
12
+ }
13
+
14
+ onRequest(config) {
15
+ // Log the transition state before sending (SDK Sec 7.D Telemetry)
16
+ logger.info({
17
+ msg: 'API Request Started',
18
+ requestId: config.requestId,
19
+ service: config.service,
20
+ method: config.method,
21
+ dropoff_marker: 'REQUEST_INIT'
22
+ });
23
+
24
+ this.requestHooks.forEach(hook => hook(config));
25
+ return config;
26
+ }
27
+
28
+ onResponse(response, result) {
29
+ this.responseHooks.forEach(hook => hook(response, result));
30
+ return result;
31
+ }
32
+
33
+ onError(error) {
34
+ logger.error({
35
+ msg: 'API Request Failed',
36
+ requestId: error.requestId,
37
+ code: error.code,
38
+ message: error.message
39
+ });
40
+
41
+ this.errorHooks.forEach(hook => hook(error));
42
+ return error;
43
+ }
44
+
45
+ addRequestHook(hook) { this.requestHooks.push(hook); }
46
+ addResponseHook(hook) { this.responseHooks.push(hook); }
47
+ addErrorHook(hook) { this.errorHooks.push(hook); }
48
+ }
49
+
50
+ module.exports = Interceptors;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Monitors backend connection health and latency (SDK Sec 10.A)
3
+ */
4
+ class NetworkMonitor {
5
+ constructor() {
6
+ this.history = [];
7
+ this.status = 'stable';
8
+ }
9
+
10
+ record(latency, success = true) {
11
+ this.history.push({ latency, timestamp: Date.now(), success });
12
+ if (this.history.length > 10) this.history.shift(); // SDK uses 10 for window
13
+
14
+ this._updateStatus();
15
+ }
16
+
17
+ _updateStatus() {
18
+ if (this.history.length === 0) {
19
+ this.status = 'stable';
20
+ return;
21
+ }
22
+
23
+ const avgLatency = this.getAverageLatency();
24
+ const failureCount = this.history.filter(h => !h.success).length;
25
+
26
+ // SDK Sec 10.A Logic: High avg latency or recent failures trigger flaky status
27
+ if (avgLatency > 5000 || failureCount > 2) {
28
+ this.status = 'flaky';
29
+ } else {
30
+ this.status = 'stable';
31
+ }
32
+ }
33
+
34
+ getAverageLatency() {
35
+ if (this.history.length === 0) return 0;
36
+ const sum = this.history.reduce((a, b) => a + b.latency, 0);
37
+ return sum / this.history.length;
38
+ }
39
+ }
40
+
41
+ module.exports = new NetworkMonitor();