@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/API_CLIENT_DESIGN.md +1313 -0
- package/package.json +16 -0
- package/src/AstrologyApiClient.js +254 -0
- package/src/config.js +27 -0
- package/src/index.js +7 -0
- package/src/utils/ApiClientError.js +73 -0
- package/src/utils/CircuitBreaker.js +48 -0
- package/src/utils/ContextStore.js +107 -0
- package/src/utils/DiagnosticsTracker.js +52 -0
- package/src/utils/Interceptors.js +50 -0
- package/src/utils/NetworkMonitor.js +41 -0
- package/src/utils/RequestBatcher.js +28 -0
- package/src/utils/ResponseDetective.js +24 -0
- package/src/utils/formatter.js +43 -0
- package/src/utils/retry.js +34 -0
|
@@ -0,0 +1,1313 @@
|
|
|
1
|
+
# Astrology API Client (SDK) - Technical Specification (Absolute Master Version)
|
|
2
|
+
|
|
3
|
+
## 1. Core Philosophy: The "Secretary" and "Mailroom" Model
|
|
4
|
+
The `api-client` is a "Universal" JavaScript SDK designed for Node.js (WhatsApp Bot), Browser (PWA), and Mobile (React Native). It acts as a **smart wrapper** around the Backend REST API.
|
|
5
|
+
|
|
6
|
+
It follows the **Secretary Model**:
|
|
7
|
+
* **Outgoing:** It fills out the "Boring Forms" (Metadata, IDs, Timestamps) so the Frontend developer doesn't have to. It acts as a **Mailroom**, wrapping business data in a formal envelope with a tracking number (`requestId`), date stamp, and sender ID.
|
|
8
|
+
* **Incoming:** It opens the "Mail" (Responses). If it's a "Bill" (Error), it alerts you immediately via a specific Exception. If it's a "Check" (Data), it hands you the clean business data. To maintain traceability, it uses **Smart Unwrapping**, attaching the formal envelope metadata as a non-enumerable property so it doesn't clutter the business logic but is available for debugging/logging (e.g., `data.metadata.requestId`).
|
|
9
|
+
|
|
10
|
+
### Design Choice: The Client as a "Shield"
|
|
11
|
+
The `api-client` acts as a **Shield**, protecting the frontend logic from the underlying complexity of backend schemas and validation.
|
|
12
|
+
* **Decoupling:** Frontend developers stay "dumb" to the mechanics of AJV or JSON schema loading. They simply call methods and catch standardized errors.
|
|
13
|
+
* **Responsibility Split:**
|
|
14
|
+
* **`shared/schemas` (The Law):** Defines the rules and data structures.
|
|
15
|
+
* **`api-client` (The Police):** Enforces the rules and handles the "arrests" (validation failures).
|
|
16
|
+
* **Frontend (The Citizen):** Enjoys the results without worrying about the legal/technical details.
|
|
17
|
+
|
|
18
|
+
### The REST-Aligned Proxy Architecture
|
|
19
|
+
* **Dynamic Proxy:** Uses JavaScript `Proxy` to intercept `client.service.method()` calls and translates them to `POST /api/v1/service/method`.
|
|
20
|
+
* **Zero Latency:** Dynamic lookup happens entirely in the client's memory (RAM). The micro-overhead (approx 0.000005ms) is invisible compared to network time (~100-500ms).
|
|
21
|
+
* **Zero Maintenance:** 100% coverage of backend services is guaranteed. New backend methods are immediately available without SDK updates.
|
|
22
|
+
* **Backend Driven:** The client automatically maps calls to the REST structure expected by the backend (`/:service/:method`).
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 2. Monorepo Integration & Directory Structure
|
|
27
|
+
|
|
28
|
+
Location: `packages/api-client/`
|
|
29
|
+
Dependencies: `@astrology/shared` (Strictly reuses existing schemas and validators).
|
|
30
|
+
|
|
31
|
+
### File Map & Component Responsibilities
|
|
32
|
+
```
|
|
33
|
+
/packages/api-client/
|
|
34
|
+
├── package.json # Defines @astrology/api-client; Depends on @astrology/shared
|
|
35
|
+
├── README.md # Full Technical Manual & Usage Examples
|
|
36
|
+
└── src/
|
|
37
|
+
├── index.js # Entry point; Exports AstrologyApiClient
|
|
38
|
+
├── AstrologyApiClient.js # Core logic; Proxy Implementation; Request Orchestration
|
|
39
|
+
├── config.js # Base URL (default :10000), Timeouts, HWM Thresholds (80%)
|
|
40
|
+
└── utils/
|
|
41
|
+
├── ApiClientError.js # Granular Classes: ApiValidationError, ApiNetworkError, ApiServerError
|
|
42
|
+
├── ContextStore.js # LRU Logic; Doubly Linked List; O(1) Complexity
|
|
43
|
+
├── RequestBatcher.js # Network Optimization; Groups multiple calls into one POST
|
|
44
|
+
├── CircuitBreaker.js # Fail-fast resilience; Threshold: 5 failures; Timeout: 30s
|
|
45
|
+
├── Interceptors.js # Global Hooks (onRequest, onResponse, onError)
|
|
46
|
+
├── ResponseDetective.js # Detection: JSON (Standard), Text (AI), Binary (PDF/Charts)
|
|
47
|
+
├── retry.js # Smart retry logic; Exponential backoff (500ms -> 1s -> 2s)
|
|
48
|
+
├── formatter.js # Message & Data formatting; Consistent UI structures
|
|
49
|
+
├── DiagnosticsTracker.js # Observability: Error & Usage Aggregation
|
|
50
|
+
└── NetworkMonitor.js # Network condition & adaptive logic
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 3. Dynamic Service Discovery (Proxy Implementation)
|
|
56
|
+
|
|
57
|
+
The client uses nested Proxies to map method calls dynamically. This ensures that even if a new method is added to the backend, the client can call it instantly without a code update.
|
|
58
|
+
|
|
59
|
+
The actual backend implementation includes several key enhancements that should be reflected in the API client design:
|
|
60
|
+
|
|
61
|
+
**Session Management Enhancements:**
|
|
62
|
+
- **Auto-creation of sessions**: The backend automatically creates sessions if they don't exist, rather than failing
|
|
63
|
+
- **Enhanced `getSessionState()` method**: Now auto-creates sessions if not found, ensuring continuity
|
|
64
|
+
- **Improved error recovery**: More robust fallback mechanisms when sessions aren't found
|
|
65
|
+
|
|
66
|
+
**Identity Resolution Improvements:**
|
|
67
|
+
- **Deterministic unified ID format**: Changed from `unified_{timestamp}_{platform_abbrev}_{random_hash}` to `unified_{platformAbbr}_{identityHash}` for consistency
|
|
68
|
+
- **Enhanced phone canonicalization**: Better validation and error handling for phone numbers
|
|
69
|
+
- **Improved fallback mechanisms**: Better handling when user mapping fails
|
|
70
|
+
|
|
71
|
+
**Schema Validation Updates:**
|
|
72
|
+
- **ServiceRegistry integration**: Automatic schema inference based on service/method names
|
|
73
|
+
- **Enhanced validation middleware**: More sophisticated validation with custom mappings
|
|
74
|
+
- **Better error responses**: More detailed validation error messages
|
|
75
|
+
|
|
76
|
+
**Error Handling Improvements:**
|
|
77
|
+
- **Comprehensive custom error classes**: Detailed AstrologyError subclasses with proper HTTP status codes
|
|
78
|
+
- **Better error response format**: More consistent responses with proper metadata
|
|
79
|
+
- **Enhanced observability**: Better integration with observability services
|
|
80
|
+
|
|
81
|
+
**API Structure:**
|
|
82
|
+
- **Method-based routing**: Fully implemented at `/api/v1/:service/:method`
|
|
83
|
+
- **Health checks**: Comprehensive health check endpoints at `/api/v1/health`
|
|
84
|
+
- **Documentation endpoints**: Available API documentation at `/api/v1/docs`
|
|
85
|
+
- **Service discovery**: Available at `/api/v1/:service/methods` and `/api/v1/:service/methods/:method`
|
|
86
|
+
|
|
87
|
+
### Detailed Implementation Snippet
|
|
88
|
+
```javascript
|
|
89
|
+
// src/AstrologyApiClient.js implementation logic
|
|
90
|
+
class AstrologyApiClient {
|
|
91
|
+
constructor(config) {
|
|
92
|
+
this.config = config;
|
|
93
|
+
this.baseUrl = config.baseUrl || 'http://localhost:10000';
|
|
94
|
+
this.contextStore = new ContextStore(config.memory);
|
|
95
|
+
this.circuitBreaker = new CircuitBreaker();
|
|
96
|
+
this.interceptors = new Interceptors();
|
|
97
|
+
|
|
98
|
+
// The Root Proxy
|
|
99
|
+
return new Proxy(this, {
|
|
100
|
+
get: (target, serviceName) => {
|
|
101
|
+
// 1. If property exists on class (like 'configure' or 'asUser'), return it
|
|
102
|
+
if (serviceName in target) return target[serviceName];
|
|
103
|
+
|
|
104
|
+
// 2. Otherwise, assume it's a backend service (e.g., 'dasha', 'birthchart')
|
|
105
|
+
// and return a secondary Service Proxy
|
|
106
|
+
return new Proxy({}, {
|
|
107
|
+
get: (_, methodName) => {
|
|
108
|
+
// 3. Return an async function that performs the actual POST request
|
|
109
|
+
return async (params = {}) => {
|
|
110
|
+
// Note: backend expects direct routing /api/v1/serviceName/methodName
|
|
111
|
+
return target._executeRequest(serviceName, methodName, params);
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async _executeRequest(service, method, data, context = null) {
|
|
120
|
+
// Full Orchestration:
|
|
121
|
+
// 1. Build Envelope (Section 5)
|
|
122
|
+
// 2. Local Validation (Section 6)
|
|
123
|
+
// 3. Circuit Check (Section 7.C)
|
|
124
|
+
// 4. Interceptor onRequest Hook (Section 11.B)
|
|
125
|
+
// 5. Execute with Smart Retry (Section 7.B) - POST /api/v1/${service}/${method}
|
|
126
|
+
// 6. Metadata Sync & Handshake (Section 4.B)
|
|
127
|
+
// 7. Response Detective Format Analysis (Section 11.A)
|
|
128
|
+
// 8. Error Wrapping or Data Unwrapping (Section 11.B)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## 3.1 Implementation: AstrologyApiClient.js
|
|
134
|
+
|
|
135
|
+
```javascript
|
|
136
|
+
// packages/api-client/src/AstrologyApiClient.js
|
|
137
|
+
// Final Production Implementation satisfying Section 3 and 4 requirements
|
|
138
|
+
const { v4: uuid } = require('uuid');
|
|
139
|
+
|
|
140
|
+
class AstrologyApiClient {
|
|
141
|
+
constructor(config = {}) {
|
|
142
|
+
this.baseUrl = config.baseUrl || 'http://localhost:10000';
|
|
143
|
+
this.config = config;
|
|
144
|
+
|
|
145
|
+
// Proxy logic as defined in Section 3
|
|
146
|
+
return new Proxy(this, {
|
|
147
|
+
get: (target, serviceName) => {
|
|
148
|
+
if (serviceName in target) return target[serviceName];
|
|
149
|
+
return new Proxy({}, {
|
|
150
|
+
get: (_, methodName) => {
|
|
151
|
+
return async (params = {}) => target.call(serviceName, methodName, params);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
asUser(platformUserId) {
|
|
159
|
+
let unifiedUserId = null;
|
|
160
|
+
let currentSessionId = null;
|
|
161
|
+
|
|
162
|
+
const ensureIdentity = async () => {
|
|
163
|
+
if (!unifiedUserId) {
|
|
164
|
+
// Reality: userMapping/mapPlatformToUnified (per Client Integration Guide)
|
|
165
|
+
const response = await fetch(`${this.baseUrl}/api/v1/userMapping/mapPlatformToUnified`, {
|
|
166
|
+
method: 'POST',
|
|
167
|
+
headers: { 'Content-Type': 'application/json' },
|
|
168
|
+
body: JSON.stringify({
|
|
169
|
+
platform: this.config.platform,
|
|
170
|
+
platformUserId,
|
|
171
|
+
requestId: uuid(),
|
|
172
|
+
timestamp: new Date().toISOString(),
|
|
173
|
+
client: {
|
|
174
|
+
type: this.config.clientType || 'api-client',
|
|
175
|
+
version: this.config.clientVersion || '1.0.0'
|
|
176
|
+
},
|
|
177
|
+
// Align with integration guide: include all required fields
|
|
178
|
+
initialProfileData: {}
|
|
179
|
+
})
|
|
180
|
+
});
|
|
181
|
+
const result = await response.json();
|
|
182
|
+
if (result.status === 'error') throw new Error(result.error.message);
|
|
183
|
+
unifiedUserId = result.data.unifiedUserId;
|
|
184
|
+
// Update session ID if provided in response
|
|
185
|
+
if (result.data.sessionId) {
|
|
186
|
+
currentSessionId = result.data.sessionId;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return unifiedUserId;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
// Internal getters for consistent request payloads
|
|
194
|
+
_getUnifiedUserId: () => unifiedUserId,
|
|
195
|
+
_getPlatformUserId: () => platformUserId,
|
|
196
|
+
|
|
197
|
+
// Observability Interface: Direct routing to Backend ObservabilityService
|
|
198
|
+
observability: {
|
|
199
|
+
async incrementCounter(name, tags = {}) {
|
|
200
|
+
const userId = await ensureIdentity();
|
|
201
|
+
// Use the proxy architecture to call the backend observability service
|
|
202
|
+
return await fetch(`${this.baseUrl}/api/v1/observability/recordMetric`, {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
headers: { 'Content-Type': 'application/json' },
|
|
205
|
+
body: JSON.stringify({
|
|
206
|
+
unifiedUserId: userId,
|
|
207
|
+
type: 'counter',
|
|
208
|
+
name,
|
|
209
|
+
tags: { ...tags, platform: this.config.platform },
|
|
210
|
+
requestId: uuid(),
|
|
211
|
+
timestamp: new Date().toISOString(),
|
|
212
|
+
client: {
|
|
213
|
+
type: this.config.clientType || 'api-client',
|
|
214
|
+
version: this.config.clientVersion || '1.0.0'
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
async recordHistogram(name, value, tags = {}) {
|
|
221
|
+
const userId = await ensureIdentity();
|
|
222
|
+
// Use the proxy architecture to call the backend observability service
|
|
223
|
+
return await fetch(`${this.baseUrl}/api/v1/observability/recordMetric`, {
|
|
224
|
+
method: 'POST',
|
|
225
|
+
headers: { 'Content-Type': 'application/json' },
|
|
226
|
+
body: JSON.stringify({
|
|
227
|
+
unifiedUserId: userId,
|
|
228
|
+
type: 'histogram',
|
|
229
|
+
name,
|
|
230
|
+
value,
|
|
231
|
+
tags: { ...tags, platform: this.config.platform },
|
|
232
|
+
requestId: uuid(),
|
|
233
|
+
timestamp: new Date().toISOString(),
|
|
234
|
+
client: {
|
|
235
|
+
type: this.config.clientType || 'api-client',
|
|
236
|
+
version: this.config.clientVersion || '1.0.0'
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
// Session access
|
|
244
|
+
async getSession() {
|
|
245
|
+
const userId = await ensureIdentity();
|
|
246
|
+
// Reality: sessionManager/getSessionState
|
|
247
|
+
const response = await fetch(`${this.baseUrl}/api/v1/sessionManager/getSessionState`, {
|
|
248
|
+
method: 'POST',
|
|
249
|
+
headers: { 'Content-Type': 'application/json' },
|
|
250
|
+
body: JSON.stringify({
|
|
251
|
+
unifiedUserId: userId,
|
|
252
|
+
requestId: uuid(),
|
|
253
|
+
timestamp: new Date().toISOString(),
|
|
254
|
+
client: {
|
|
255
|
+
type: this.config.clientType || 'api-client',
|
|
256
|
+
version: this.config.clientVersion || '1.0.0'
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
});
|
|
260
|
+
const result = await response.json();
|
|
261
|
+
if (result.status === 'error') throw new Error(result.error.message);
|
|
262
|
+
currentSessionId = result.data.sessionId;
|
|
263
|
+
|
|
264
|
+
// Smart Unwrapping: Attach metadata as non-enumerable property
|
|
265
|
+
if (result.data && typeof result.data === 'object') {
|
|
266
|
+
Object.defineProperty(result.data, 'metadata', { value: result.metadata, enumerable: false });
|
|
267
|
+
}
|
|
268
|
+
return result.data;
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
// Lock management
|
|
272
|
+
async acquireSessionLock() {
|
|
273
|
+
const userId = await ensureIdentity();
|
|
274
|
+
const response = await fetch(`${this.baseUrl}/api/v1/sessionManager/acquireSessionLock`, {
|
|
275
|
+
method: 'POST',
|
|
276
|
+
headers: { 'Content-Type': 'application/json' },
|
|
277
|
+
body: JSON.stringify({
|
|
278
|
+
unifiedUserId: userId,
|
|
279
|
+
requestId: uuid(),
|
|
280
|
+
timestamp: new Date().toISOString(),
|
|
281
|
+
client: {
|
|
282
|
+
type: this.config.clientType || 'api-client',
|
|
283
|
+
version: this.config.clientVersion || '1.0.0'
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
});
|
|
287
|
+
const result = await response.json();
|
|
288
|
+
if (result.status === 'error') throw new Error(result.error.message);
|
|
289
|
+
return true;
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
async releaseSessionLock() {
|
|
293
|
+
const userId = await ensureIdentity();
|
|
294
|
+
const response = await fetch(`${this.baseUrl}/api/v1/sessionManager/releaseSessionLock`, {
|
|
295
|
+
method: 'POST',
|
|
296
|
+
headers: { 'Content-Type': 'application/json' },
|
|
297
|
+
body: JSON.stringify({
|
|
298
|
+
unifiedUserId: userId,
|
|
299
|
+
requestId: uuid(),
|
|
300
|
+
timestamp: new Date().toISOString(),
|
|
301
|
+
client: {
|
|
302
|
+
type: this.config.clientType || 'api-client',
|
|
303
|
+
version: this.config.clientVersion || '1.0.0'
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
});
|
|
307
|
+
const result = await response.json();
|
|
308
|
+
if (result.status === 'error') throw new Error(result.error.message);
|
|
309
|
+
return true;
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
// Session saving
|
|
313
|
+
async save(partialState) {
|
|
314
|
+
const userId = await ensureIdentity();
|
|
315
|
+
const response = await fetch(`${this.baseUrl}/api/v1/sessionManager/updateSessionState`, {
|
|
316
|
+
method: 'POST',
|
|
317
|
+
headers: { 'Content-Type': 'application/json' },
|
|
318
|
+
body: JSON.stringify({
|
|
319
|
+
unifiedUserId: userId,
|
|
320
|
+
sessionId: currentSessionId,
|
|
321
|
+
partialState,
|
|
322
|
+
requestId: uuid(),
|
|
323
|
+
timestamp: new Date().toISOString(),
|
|
324
|
+
client: {
|
|
325
|
+
type: this.config.clientType || 'api-client',
|
|
326
|
+
version: this.config.clientVersion || '1.0.0'
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
});
|
|
330
|
+
const result = await response.json();
|
|
331
|
+
if (result.status === 'error') throw new Error(result.error.message);
|
|
332
|
+
|
|
333
|
+
// Smart Unwrapping: Attach metadata as non-enumerable property
|
|
334
|
+
if (result.data && typeof result.data === 'object') {
|
|
335
|
+
Object.defineProperty(result.data, 'metadata', { value: result.metadata, enumerable: false });
|
|
336
|
+
}
|
|
337
|
+
await this.releaseSessionLock();
|
|
338
|
+
return result.data;
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
// Service calls
|
|
342
|
+
async call(service, method, params = {}) {
|
|
343
|
+
const userId = await ensureIdentity();
|
|
344
|
+
// Reality: direct routing /api/v1/:service/:method with comprehensive platform context
|
|
345
|
+
const response = await fetch(`${this.baseUrl}/api/v1/${service}/${method}`, {
|
|
346
|
+
method: 'POST',
|
|
347
|
+
headers: { 'Content-Type': 'application/json' },
|
|
348
|
+
body: JSON.stringify({
|
|
349
|
+
unifiedUserId: userId,
|
|
350
|
+
platform: this.config.platform,
|
|
351
|
+
platformUserId,
|
|
352
|
+
requestId: uuid(),
|
|
353
|
+
timestamp: new Date().toISOString(),
|
|
354
|
+
client: {
|
|
355
|
+
type: this.config.clientType || 'api-client',
|
|
356
|
+
version: this.config.clientVersion || '1.0.0'
|
|
357
|
+
},
|
|
358
|
+
// Include additional context fields required by integration guide
|
|
359
|
+
initialProfileData: params.initialProfileData || {},
|
|
360
|
+
preferences: params.preferences || {},
|
|
361
|
+
birthData: params.birthData || {},
|
|
362
|
+
parameters: params.parameters || {},
|
|
363
|
+
...params
|
|
364
|
+
})
|
|
365
|
+
});
|
|
366
|
+
const result = await response.json();
|
|
367
|
+
if (result.status === 'error') throw new Error(result.error.message);
|
|
368
|
+
|
|
369
|
+
// Smart Unwrapping: Hand clean data but attach metadata as non-enumerable property for tracing
|
|
370
|
+
if (result.data && typeof result.data === 'object') {
|
|
371
|
+
Object.defineProperty(result.data, 'metadata', {
|
|
372
|
+
value: result.metadata,
|
|
373
|
+
enumerable: false
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
return result.data;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
module.exports = AstrologyApiClient;
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## 4. Multi-User Identity & Session Isolation
|
|
388
|
+
|
|
389
|
+
**CRITICAL:** To prevent data leakage in multi-user environments (WhatsApp), the client enforces strict context isolation. In a Node.js process, a single client instance with shared state would lead to User A's data leaking into User B's request.
|
|
390
|
+
|
|
391
|
+
### A. Scoped Context Instances (`asUser`)
|
|
392
|
+
The client uses a "Factory" pattern to create isolated instances for each unique user or conversation.
|
|
393
|
+
* **Isolation:** `client.asUser(id)` creates a unique instance with its own `sessionId` and `unifiedUserId`.
|
|
394
|
+
* **Security:** This ensures User A's metadata can **never** be sent in User B's request.
|
|
395
|
+
|
|
396
|
+
```javascript
|
|
397
|
+
// Scoped Factory Logic Implementation
|
|
398
|
+
asUser(platformUserId) {
|
|
399
|
+
let context = this.contextStore.get(platformUserId); // O(1) Lookup
|
|
400
|
+
if (!context) {
|
|
401
|
+
context = {
|
|
402
|
+
platform: this.config.platform,
|
|
403
|
+
platformUserId,
|
|
404
|
+
sessionId: null,
|
|
405
|
+
unifiedUserId: null,
|
|
406
|
+
lastAccessed: Date.now()
|
|
407
|
+
};
|
|
408
|
+
this.contextStore.set(platformUserId, context);
|
|
409
|
+
}
|
|
410
|
+
// Returns a proxy instance bound strictly to this specific user context
|
|
411
|
+
return this._createScopedInstance(context);
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### B. Self-Healing Handshake & Auto-Sync
|
|
416
|
+
The client is **"Self-Aware"** and watches every response from the backend to ensure the frontend stays in sync without manual code.
|
|
417
|
+
1. **Auto-Resolution:** If a context lacks `unifiedUserId`, the first call automatically triggers the `platformManager.getUnifiedUserId` handshake.
|
|
418
|
+
2. **Session Recovery:** On `SESSION_EXPIRED` (401), the client re-resolves identity and **automatically retries** the request once.
|
|
419
|
+
3. **Auto-Sync:** If the backend returns a new `sessionId` or `unifiedUserId` in the metadata, the client updates the Scoped Context in memory instantly.
|
|
420
|
+
|
|
421
|
+
```javascript
|
|
422
|
+
// Metadata Sync & Handshake logic
|
|
423
|
+
async _handleSync(context, response) {
|
|
424
|
+
const { sessionId, unifiedUserId } = response.metadata || {};
|
|
425
|
+
|
|
426
|
+
// Auto-Sync updated identifiers from metadata
|
|
427
|
+
if (sessionId) context.sessionId = sessionId;
|
|
428
|
+
if (unifiedUserId) context.unifiedUserId = unifiedUserId;
|
|
429
|
+
|
|
430
|
+
// Handle Session Expiration Handshake (401)
|
|
431
|
+
if (response.status === 'error' && response.error.code === 'SESSION_EXPIRED') {
|
|
432
|
+
const resolution = await this.platformManager.getUnifiedUserId({
|
|
433
|
+
platform: context.platform,
|
|
434
|
+
platformUserId: context.platformUserId
|
|
435
|
+
});
|
|
436
|
+
context.unifiedUserId = resolution.unifiedUserId;
|
|
437
|
+
context.sessionId = resolution.sessionId;
|
|
438
|
+
return true; // Signal the orchestrator to retry the original call
|
|
439
|
+
}
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## 5. The Unified Request Envelope (Metadata & Tracing)
|
|
447
|
+
|
|
448
|
+
**`requestId` is the sole identifier used for tracing and correlation across the entire system.** We do not use separate traceIds.
|
|
449
|
+
|
|
450
|
+
### Data Injection Logic
|
|
451
|
+
* **`requestId`**: A unique UUID generated at the start of every operation. This is injected into **both the JSON payload and the `X-Request-ID` HTTP header** for end-to-end traceability through proxies, load balancers, and logs.
|
|
452
|
+
* **`timestamp`**: Current ISO-8601 string.
|
|
453
|
+
* **`client`**: Platform identifier (e.g., 'whatsapp', 'ios', 'android', 'pwa').
|
|
454
|
+
* **`sessionId` / `unifiedUserId`**: Automatically pulled from the active Scoped Context.
|
|
455
|
+
|
|
456
|
+
```javascript
|
|
457
|
+
// Envelope Packaging Implementation
|
|
458
|
+
_buildEnvelope(context, businessData) {
|
|
459
|
+
const requestId = uuid();
|
|
460
|
+
return {
|
|
461
|
+
payload: {
|
|
462
|
+
requestId,
|
|
463
|
+
timestamp: new Date().toISOString(),
|
|
464
|
+
client: context ? context.platform : this.config.platform,
|
|
465
|
+
sessionId: context ? context.sessionId : null,
|
|
466
|
+
unifiedUserId: context ? context.unifiedUserId : null,
|
|
467
|
+
...businessData
|
|
468
|
+
},
|
|
469
|
+
headers: {
|
|
470
|
+
'X-Request-ID': requestId,
|
|
471
|
+
'Content-Type': 'application/json',
|
|
472
|
+
'Authorization': `Bearer ${this.config.apiKey}` // Generic Auth Hook
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## 6. Detailed Internal Data Flow
|
|
481
|
+
|
|
482
|
+
| Step | Action | Responsibility | Logic Detail |
|
|
483
|
+
| :--- | :--- | :--- | :--- |
|
|
484
|
+
| **1. Call** | `user.dasha.get()` | Proxy | Intercept property access. |
|
|
485
|
+
| **2. Check** | Identity Check | Manager | Check if `unifiedUserId` exists in context. |
|
|
486
|
+
| **3. Resolve** | Handshake | Handshake | If missing, call `platformManager/getUnifiedUserId` (Handshake) first. |
|
|
487
|
+
| **4. Envelope**| Packaging | Envelope | Inject `requestId`, `timestamp`, `sessionId`. |
|
|
488
|
+
| **5. Header** | Tracing | Interceptor | Inject `X-Request-ID` and generic Auth headers. |
|
|
489
|
+
| **6. Validate** | Validation | Shared Lib | Check input against `@astrology/shared` AJV schemas. |
|
|
490
|
+
| **7. Request** | Execution | retry.js | Execute `fetch` (`/:service/:method`) with status-based retry logic. |
|
|
491
|
+
| **8. Detect** | Analysis | Detective | Check `Content-Type` (JSON vs Text vs Binary). |
|
|
492
|
+
| **9. Parse** | Processing | Shared Lib | If error, wrap in `ApiClientError`. If success, unwrap envelope. |
|
|
493
|
+
| **10. Sync** | Updating | Sync | If backend issues new `sessionId`, update Context in memory. |
|
|
494
|
+
| **11. Return** | Final Result | Proxy | Hand clean business data back to the UI. |
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## 7. Advanced Reliability & Error Handling
|
|
499
|
+
|
|
500
|
+
### A. AJV Path Mapping (ApiValidationError)
|
|
501
|
+
The client translates raw AJV paths (e.g., `/birthData/time`) into clean UI-friendly nested objects, allowing the frontend to highlight specific fields easily.
|
|
502
|
+
|
|
503
|
+
**Design Choice: The Client as a "Translator"**
|
|
504
|
+
* **Problem:** AJV returns "computer-speak" like `instancePath: "/birthData/date"`.
|
|
505
|
+
* **Solution:** The client translates this into "human-speak" like `{ birthData: { date: "Must be DD/MM/YYYY" } }`.
|
|
506
|
+
* **Result:** The UI developer can simply read the clean object and display the message directly on the corresponding input field.
|
|
507
|
+
|
|
508
|
+
```javascript
|
|
509
|
+
// src/utils/ApiClientError.js
|
|
510
|
+
class ApiValidationError extends Error {
|
|
511
|
+
constructor(ajvErrors) {
|
|
512
|
+
super("Input Validation Failed");
|
|
513
|
+
this.name = 'ApiValidationError';
|
|
514
|
+
this.mappedErrors = {};
|
|
515
|
+
|
|
516
|
+
ajvErrors.forEach(err => {
|
|
517
|
+
// Translates "/birthData/time" -> ['birthData', 'time']
|
|
518
|
+
const path = err.instancePath.split('/').filter(Boolean);
|
|
519
|
+
let current = this.mappedErrors;
|
|
520
|
+
|
|
521
|
+
// Navigate/build nested object structure
|
|
522
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
523
|
+
current[path[i]] = current[path[i]] || {};
|
|
524
|
+
current = current[path[i]];
|
|
525
|
+
}
|
|
526
|
+
// Assign message: { birthData: { time: "Required field missing" } }
|
|
527
|
+
current[path[path.length - 1]] = err.message;
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### B. Smart Retry Strategy
|
|
534
|
+
Not all errors are retryable. The client intelligently decides whether to retry or abort to save bandwidth and server resources.
|
|
535
|
+
* **RETRY (Exponential Backoff: 500ms -> 1s -> 2s):**
|
|
536
|
+
* Status 5xx (Internal Server Errors)
|
|
537
|
+
* Status 408 (Request Timeout)
|
|
538
|
+
* Network Connectivity failures
|
|
539
|
+
* **ABORT (Immediate Failure):**
|
|
540
|
+
* Status 400 (Validation Errors)
|
|
541
|
+
* Status 403 (Forbidden)
|
|
542
|
+
* Status 404 (Not Found)
|
|
543
|
+
* **Special:** Status 401 triggers the Handshake logic before retrying.
|
|
544
|
+
|
|
545
|
+
```javascript
|
|
546
|
+
// src/utils/retry.js implementation
|
|
547
|
+
const RETRYABLE_STATUSES = [408, 500, 502, 503, 504];
|
|
548
|
+
|
|
549
|
+
async function executeWithRetry(fn, context) {
|
|
550
|
+
let attempt = 0;
|
|
551
|
+
while (attempt < 3) {
|
|
552
|
+
try {
|
|
553
|
+
return await fn();
|
|
554
|
+
} catch (error) {
|
|
555
|
+
// Only retry if status is in list or it's a network-level failure
|
|
556
|
+
if (!RETRYABLE_STATUSES.includes(error.status) && !error.isNetwork) throw error;
|
|
557
|
+
attempt++;
|
|
558
|
+
const delay = Math.pow(2, attempt) * 500; // Exponentially increasing wait
|
|
559
|
+
await sleep(delay);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
### C. Circuit Breaker Logic
|
|
566
|
+
If a specific service (e.g., `dasha`) fails 5 consecutive times, the "circuit" trips for 30 seconds. All subsequent calls fail instantly at the client level with a `CircuitBreakerError` without hitting the network, protecting the backend from cascading failure.
|
|
567
|
+
|
|
568
|
+
```javascript
|
|
569
|
+
// src/utils/CircuitBreaker.js
|
|
570
|
+
class CircuitBreaker {
|
|
571
|
+
constructor() {
|
|
572
|
+
this.failures = new Map(); // serviceName -> count
|
|
573
|
+
this.lastFailure = new Map(); // serviceName -> timestamp
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
check(service) {
|
|
578
|
+
if (this.failures.get(service) >= 5) {
|
|
579
|
+
const elapsed = Date.now() - this.lastFailure.get(service);
|
|
580
|
+
if (elapsed < 30000) throw new CircuitBreakerError(`${service} tripped`);
|
|
581
|
+
// Half-Open: Allow 1 request through after 30s
|
|
582
|
+
// If it succeeds, reset. If it fails, trip again.
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
recordSuccess(service) {
|
|
587
|
+
this.failures.set(service, 0); // Reset on success
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
recordFailure(service) {
|
|
591
|
+
const count = (this.failures.get(service) || 0) + 1;
|
|
592
|
+
this.failures.set(service, count);
|
|
593
|
+
this.lastFailure.set(service, Date.now());
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### D. Telemetry & User Drop-off Logs
|
|
599
|
+
**Improvement:** The client aggressively logs `requestId` at every step to allow backend correlation.
|
|
600
|
+
|
|
601
|
+
```javascript
|
|
602
|
+
// src/utils/Interceptors.js
|
|
603
|
+
onRequest(config) {
|
|
604
|
+
// Log the transition state before sending
|
|
605
|
+
logger.info({
|
|
606
|
+
msg: 'API Request Started',
|
|
607
|
+
requestId: config.headers['X-Request-ID'],
|
|
608
|
+
service: config.url,
|
|
609
|
+
// CRITICAL: Log who is attempting this call
|
|
610
|
+
userId: config.unifiedUserId,
|
|
611
|
+
dropoff_marker: 'REQUEST_INIT' // Used to find where users stop
|
|
612
|
+
});
|
|
613
|
+
return config;
|
|
614
|
+
}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
### D. Comprehensive Client Error Catalog
|
|
618
|
+
|
|
619
|
+
The `api-client` maps specific backend error codes (as defined in `CLIENT_INTEGRATION_GUIDE.md` Sections 8.2 and 13) into distinct `ApiClientError` subclasses. This allows frontend clients to implement precise UI/UX responses without parsing raw backend messages.
|
|
620
|
+
|
|
621
|
+
| Backend Error Code | `ApiClientError` Subclass | HTTP Status | Client UI Action / Resolution |
|
|
622
|
+
| :--- | :--- | :--- | :--- |
|
|
623
|
+
| `VALIDATION_ERROR` | `ApiValidationError` | 400 | Show detailed field-specific errors. |
|
|
624
|
+
| `INVALID_BIRTH_DATA` | `ApiInvalidBirthDataError` | 400 | Prompt user to check birth details. |
|
|
625
|
+
| `RATE_LIMIT_EXCEEDED`| `ApiRateLimitError`| 429 | Back off and warn user of high frequency. |
|
|
626
|
+
| `EPHEMERIS_UNAVAILABLE`| `ApiEphemerisError` | 503 | "Calculation engine error," general retry. |
|
|
627
|
+
| `AI_SERVICE_ERROR` | `ApiAIServiceError` | 503 | "Interpretation unavailable," show raw data only. |
|
|
628
|
+
| `TIMEOUT_ERROR` | `ApiTimeoutError` | 504 | "Operation timed out," offer retry. |
|
|
629
|
+
| `CALCULATION_ERROR` | `ApiCalculationError` | 500 | Generic calculation failure, standard retry. |
|
|
630
|
+
| `DATABASE_ERROR` | `ApiDatabaseError` | 500 | Application persistence error, standard retry. |
|
|
631
|
+
| `INTERNAL_ERROR` | `ApiInternalError` | 500 | Log `requestId`, show general retry message. |
|
|
632
|
+
|
|
633
|
+
```javascript
|
|
634
|
+
// src/utils/ApiClientError.js (Extended)
|
|
635
|
+
class ApiClientError extends Error {
|
|
636
|
+
constructor(message, code, details = {}) {
|
|
637
|
+
super(message);
|
|
638
|
+
this.name = this.constructor.name;
|
|
639
|
+
this.code = code;
|
|
640
|
+
this.details = details;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
class ApiValidationError extends ApiClientError { /* ... existing implementation ... */ }
|
|
645
|
+
class ApiInvalidBirthDataError extends ApiClientError { constructor(msg, details) { super(msg || "Invalid birth data", "INVALID_BIRTH_DATA", details); } }
|
|
646
|
+
class ApiRateLimitError extends ApiClientError { constructor(msg, details) { super(msg || "Rate limit exceeded", "RATE_LIMIT_EXCEEDED", details); } }
|
|
647
|
+
class ApiEphemerisError extends ApiClientError { constructor(msg, details) { super(msg || "Ephemeris service unavailable", "EPHEMERIS_UNAVAILABLE", details); } }
|
|
648
|
+
class ApiAIServiceError extends ApiClientError { constructor(msg, details) { super(msg || "AI service unavailable", "AI_SERVICE_ERROR", details); } }
|
|
649
|
+
class ApiTimeoutError extends ApiClientError { constructor(msg, details) { super(msg || "Operation timed out", "TIMEOUT_ERROR", details); } }
|
|
650
|
+
class ApiCalculationError extends ApiClientError { constructor(msg, details) { super(msg || "Calculation failed", "CALCULATION_ERROR", details); } }
|
|
651
|
+
class ApiDatabaseError extends ApiClientError { constructor(msg, details) { super(msg || "Database error", "DATABASE_ERROR", details); } }
|
|
652
|
+
class ApiInternalError extends ApiClientError { constructor(msg, details) { super(msg || "Internal server error", "INTERNAL_ERROR", details); } }
|
|
653
|
+
|
|
654
|
+
// The _executeRequest method (or similar) within AstrologyApiClient would translate HTTP errors:
|
|
655
|
+
// ...
|
|
656
|
+
// if (response.status === 400 && result.error.code === 'VALIDATION_ERROR') {
|
|
657
|
+
// throw new ApiValidationError(result.error.details.validationErrors);
|
|
658
|
+
// } else if (response.status === 503 && result.error.code === 'EPHEMERIS_UNAVAILABLE') {
|
|
659
|
+
// throw new ApiEphemerisError(result.error.message);
|
|
660
|
+
// }
|
|
661
|
+
// ... and so on for other specific error codes.
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
### A. O(1) LRU Context Store
|
|
665
|
+
Scoped instances are managed by a specialized `ContextStore` using a `Map` for fast lookup and a **Doubly Linked List** for LRU eviction. This ensures O(1) performance regardless of the number of concurrent users.
|
|
666
|
+
* **Safety Threshold:** The store emits alerts if the number of active contexts exceeds a configurable limit.
|
|
667
|
+
|
|
668
|
+
```javascript
|
|
669
|
+
class ContextStore {
|
|
670
|
+
constructor(limit) {
|
|
671
|
+
this.limit = limit;
|
|
672
|
+
this.cache = new Map(); // Fast O(1) lookup
|
|
673
|
+
this.head = null; // Most Recent
|
|
674
|
+
this.tail = null; // Least Recent
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
get(id) {
|
|
678
|
+
const node = this.cache.get(id);
|
|
679
|
+
if (node) this._moveToHead(node); // Keep LRU order
|
|
680
|
+
return node?.data;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Linked List logic ensures cleanup is also O(1)
|
|
684
|
+
}
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
### B. Scalability under High Load (Memory Pressure)
|
|
688
|
+
* **High Water Mark (HWM):** The store monitors total heap usage. If usage exceeds **80%**, it enters **Aggressive Mode**.
|
|
689
|
+
* **Aggressive Mode Action:** The `Inactivity Timeout` is automatically halved (e.g., 15m -> 7.5m) to force-evict older contexts faster until memory stabilizes.
|
|
690
|
+
|
|
691
|
+
```javascript
|
|
692
|
+
// Memory Monitoring & Aggressive Eviction logic
|
|
693
|
+
setInterval(() => {
|
|
694
|
+
const memory = process.memoryUsage();
|
|
695
|
+
const usageRatio = memory.heapUsed / memory.heapTotal;
|
|
696
|
+
|
|
697
|
+
if (usageRatio > 0.8) { // 80% Threshold
|
|
698
|
+
this.emit('alert', 'Memory pressure detected. Entering Aggressive Mode.');
|
|
699
|
+
this.config.idleTimeout /= 2; // Shorten timeout
|
|
700
|
+
this.sweep(); // Run immediate cleanup cycle
|
|
701
|
+
}
|
|
702
|
+
}, 30000); // Check every 30s
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### C. Persistence Adapter & Sweep Cycle
|
|
706
|
+
* **Stateless Scaling:** Supports `PersistenceAdapter` to serialize context data to **Redis or MongoDB**. Allows multiple bot instances to share state.
|
|
707
|
+
* **Active Cleanup:** A background timer runs every 5 minutes (**Sweep Cycle**) to proactively delete contexts that have exceeded their `Idle Timeout`.
|
|
708
|
+
|
|
709
|
+
---
|
|
710
|
+
|
|
711
|
+
## 9. Performance & Observability (Deep Monitoring)
|
|
712
|
+
|
|
713
|
+
### A. Intelligent Request Caching
|
|
714
|
+
Built-in in-memory TTL cache for immutable astrology data (Birth Charts). Skips network calls for repeated identical requests within the TTL. Supports pluggable storage (Redis/LocalStorage).
|
|
715
|
+
|
|
716
|
+
### B. Request Batching
|
|
717
|
+
`client.batch([ ... ])` bundles multiple service calls into a single HTTP POST request. This is critical for reducing latency on mobile networks (3G/4G).
|
|
718
|
+
|
|
719
|
+
```javascript
|
|
720
|
+
// Request Batching logic
|
|
721
|
+
async batch(calls) {
|
|
722
|
+
const payload = {
|
|
723
|
+
requestId: uuid(),
|
|
724
|
+
isBatch: true,
|
|
725
|
+
requests: calls.map(c => ({
|
|
726
|
+
service: c.service,
|
|
727
|
+
method: c.method,
|
|
728
|
+
params: c.params
|
|
729
|
+
}))
|
|
730
|
+
};
|
|
731
|
+
const response = await fetch(`${baseUrl}/batch`, { body: JSON.stringify(payload) });
|
|
732
|
+
return response.results;
|
|
733
|
+
}
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
### C. Connection Pooling (Node.js / WhatsApp)
|
|
737
|
+
When running in Node.js, the client uses a persistent `http.Agent` with `keepAlive: true` to reduce TCP/TLS handshake overhead for high-traffic bots.
|
|
738
|
+
|
|
739
|
+
```javascript
|
|
740
|
+
const http = require('http');
|
|
741
|
+
const agentOptions = {
|
|
742
|
+
keepAlive: true,
|
|
743
|
+
maxSockets: 100,
|
|
744
|
+
freeSocketTimeout: 30000
|
|
745
|
+
};
|
|
746
|
+
this.agents = { http: new http.Agent(agentOptions) };
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### D. Comprehensive Speed Profiling
|
|
750
|
+
The client breaks down the request lifecycle into granular phases, allowing the frontend to identify where bottlenecks occur. Additional observability metrics include request success rate, error types, retry counts, and user-specific latency trends.
|
|
751
|
+
|
|
752
|
+
```javascript
|
|
753
|
+
const startTime = performance.now();
|
|
754
|
+
// 1. Preparation Phase (Local)
|
|
755
|
+
const req = this._buildEnvelope(data);
|
|
756
|
+
const t0 = performance.now();
|
|
757
|
+
|
|
758
|
+
// 2. Network Phase (Latency)
|
|
759
|
+
const response = await fetch(...);
|
|
760
|
+
const t1 = performance.now(); // TTFB (Time to First Byte)
|
|
761
|
+
|
|
762
|
+
// 3. Transfer Phase (Payload)
|
|
763
|
+
const body = await response.json();
|
|
764
|
+
const t2 = performance.now(); // Processing Finish
|
|
765
|
+
|
|
766
|
+
this.onMetrics({
|
|
767
|
+
requestId: req.payload.requestId,
|
|
768
|
+
prep_time: t0 - startTime, // Serialization + Local Validation
|
|
769
|
+
latency_ttfb: t1 - t0, // DNS + Connection + Server Waiting
|
|
770
|
+
transfer_time: t2 - t1, // Downloading & Parsing Payload
|
|
771
|
+
total_duration: t2 - startTime,
|
|
772
|
+
success: true,
|
|
773
|
+
error_type: null, // 'network', 'timeout', 'validation'
|
|
774
|
+
retry_count: 0,
|
|
775
|
+
user_id: this.context.unifiedUserId,
|
|
776
|
+
platform: this.config.platform
|
|
777
|
+
});
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
### E. Advanced Error Aggregation & Health Reporting
|
|
781
|
+
Following your `DiagnosticsTracker` pattern, the client aggregates failures by category to produce a real-time health snapshot of the SDK and Backend connection.
|
|
782
|
+
|
|
783
|
+
```javascript
|
|
784
|
+
// src/utils/DiagnosticsTracker.js logic
|
|
785
|
+
recordError(type, requestId) {
|
|
786
|
+
// Categories: CONNECTION, SERVER, VALIDATION, IDENTITY
|
|
787
|
+
const category = this._mapToCategory(type);
|
|
788
|
+
this.stats.errors[category] = (this.stats.errors[category] || 0) + 1;
|
|
789
|
+
this.stats.recent_failures.push({ category, requestId, timestamp: Date.now() });
|
|
790
|
+
|
|
791
|
+
if (this.stats.recent_failures.length > 50) this.stats.recent_failures.shift();
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
getHealthReport() {
|
|
795
|
+
return {
|
|
796
|
+
uptime: process.uptime(),
|
|
797
|
+
error_distribution: this.stats.errors,
|
|
798
|
+
avg_latency: this.stats.avg_latency,
|
|
799
|
+
network_status: this.monitor.status, // online/flaky
|
|
800
|
+
usage: {
|
|
801
|
+
total_requests: this.stats.total_requests,
|
|
802
|
+
total_bytes_sent: this.stats.total_bytes_sent,
|
|
803
|
+
total_bytes_received: this.stats.total_bytes_received
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
### F. HTTP/2 Multiplexing Support
|
|
810
|
+
Update `AstrologyApiClient.js` to use Node.js `http2` module instead of `fetch` for backend calls. Configure agent with `http2.connect()` for multiplexing. This allows concurrent streams over one connection, reducing latency for batched requests. Test with backend supporting HTTP/2.
|
|
811
|
+
|
|
812
|
+
---
|
|
813
|
+
|
|
814
|
+
## 10. Network Condition & Adaptive Logic
|
|
815
|
+
|
|
816
|
+
### A. Condition Monitoring (NetworkMonitor Implementation)
|
|
817
|
+
The client detects flaky environments and spikes in latency to adjust its behavior dynamically.
|
|
818
|
+
|
|
819
|
+
```javascript
|
|
820
|
+
// src/utils/NetworkMonitor.js
|
|
821
|
+
class NetworkMonitor {
|
|
822
|
+
constructor() {
|
|
823
|
+
this.latencyWindow = []; // Last 10 durations
|
|
824
|
+
this.status = 'stable';
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
observe(duration, success) {
|
|
828
|
+
this.latencyWindow.push(duration);
|
|
829
|
+
if (this.latencyWindow.length > 10) this.latencyWindow.shift();
|
|
830
|
+
|
|
831
|
+
const avgLatency = this.latencyWindow.reduce((a,b) => a+b, 0) / this.latencyWindow.length;
|
|
832
|
+
|
|
833
|
+
// Detect flaky network based on high average latency or recent failures
|
|
834
|
+
if (avgLatency > 5000 || !success) {
|
|
835
|
+
this.status = 'flaky';
|
|
836
|
+
} else {
|
|
837
|
+
this.status = 'stable';
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
### B. Adaptive Timeouts
|
|
844
|
+
```javascript
|
|
845
|
+
// Logic to adjust timeouts based on detected network quality
|
|
846
|
+
const getTimeout = (monitor) => {
|
|
847
|
+
if (monitor.status === 'flaky') return 60000; // Increase to 60s for flaky mobile
|
|
848
|
+
return 15000; // Standard 15s for server-to-server
|
|
849
|
+
};
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
---
|
|
853
|
+
|
|
854
|
+
## 11. Formatting & Compatibility
|
|
855
|
+
|
|
856
|
+
### A. Response Detection (ResponseDetective)
|
|
857
|
+
The client is compatible with the backend's diverse output formats (JSON, Plain Text, Binary). It checks the `Content-Type` header automatically.
|
|
858
|
+
|
|
859
|
+
```javascript
|
|
860
|
+
// Format Detection Implementation
|
|
861
|
+
async detect(response) {
|
|
862
|
+
const type = response.headers.get('content-type') || '';
|
|
863
|
+
if (type.includes('application/json')) return response.json();
|
|
864
|
+
if (type.includes('text/plain')) return response.text(); // AI Chat responses
|
|
865
|
+
return response.arrayBuffer(); // Binary (Charts/PDFs)
|
|
866
|
+
}
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
### B. Interceptors & Unwrapping
|
|
870
|
+
* **Global Interceptors:** hooks (`onRequest`, `onResponse`, `onError`) for centralized Authorization and tracing logic.
|
|
871
|
+
* **Envelope Unwrapping:** Natively handles the backend's `formatSuccessResponse` and `formatErrorResponse` utilities from `packages/backend/src/shared/responses.js`.
|
|
872
|
+
|
|
873
|
+
### C. Message Formatter Logic
|
|
874
|
+
Includes shared logic to structure chat responses (text, buttons, menus) consistently across WhatsApp and Mobile UIs, mirroring backend formatting.
|
|
875
|
+
|
|
876
|
+
### D. Schema Awareness & Type Safety
|
|
877
|
+
While the frontend logic is decoupled from schema files, the SDK provides "Type Safety" benefits:
|
|
878
|
+
* **Editor Hints:** The client can provide JSDoc/TypeScript hints about required fields based on backend schemas.
|
|
879
|
+
* **No Imports Required:** The frontend does *not* need to import `.json` schemas or AJV; the `api-client` encapsulates all enforcement and validation.
|
|
880
|
+
|
|
881
|
+
---
|
|
882
|
+
|
|
883
|
+
## 12. Usage Examples
|
|
884
|
+
|
|
885
|
+
### WhatsApp Bot: Multi-User Scoped Flow
|
|
886
|
+
```javascript
|
|
887
|
+
const astrology = new AstrologyApiClient({
|
|
888
|
+
platform: 'whatsapp',
|
|
889
|
+
persistence: new RedisAdapter(process.env.REDIS_URL),
|
|
890
|
+
memory: { maxContexts: 10000, aggressiveModeThreshold: 0.8 },
|
|
891
|
+
onMetrics: (m) => logger.info(`Metric: ${m.requestId}`, m)
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
// Inside WhatsApp message handler
|
|
895
|
+
async function onMessage(phoneNumber, text) {
|
|
896
|
+
const user = astrology.asUser(phoneNumber); // Isolated Scoped Context
|
|
897
|
+
try {
|
|
898
|
+
const dasha = await user.call('dasha', 'getDashaPredictiveAnalysis', { birthData });
|
|
899
|
+
reply(`Your current Dasha is ${dasha.mahadasha.lord}. RequestId: ${dasha.metadata.requestId}`);
|
|
900
|
+
} catch (e) {
|
|
901
|
+
if (e instanceof ApiValidationError) {
|
|
902
|
+
reply(`Field error: ${JSON.stringify(e.mappedErrors)}`);
|
|
903
|
+
} else if (e instanceof ApiRateLimitError) {
|
|
904
|
+
reply("Slow down! Too many requests.");
|
|
905
|
+
} else {
|
|
906
|
+
reply("An unexpected error occurred. Please try again.");
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
### Mobile App: Batched & Efficient
|
|
913
|
+
```javascript
|
|
914
|
+
const client = new AstrologyApiClient({ platform: 'ios', cache: { enabled: true } });
|
|
915
|
+
|
|
916
|
+
// Startup: Get everything in one network round-trip
|
|
917
|
+
const [planets, dasha, summary] = await client.batch([
|
|
918
|
+
client.birthchart.calculateBirthChart(input),
|
|
919
|
+
client.dasha.getDashaPredictiveAnalysis(input),
|
|
920
|
+
client.astrologer.getSummary(input)
|
|
921
|
+
]);
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
---
|
|
925
|
+
|
|
926
|
+
## 13. API Versioning & Backward Compatibility
|
|
927
|
+
|
|
928
|
+
To handle the evolution of the backend while supporting older frontend versions, the client implements a **Multi-Version Routing** strategy.
|
|
929
|
+
|
|
930
|
+
### A. Default & Explicit Versioning
|
|
931
|
+
The client defaults to `v1` (as configured in `config.js`), but allows overriding the version for specific calls or entire instances.
|
|
932
|
+
|
|
933
|
+
```javascript
|
|
934
|
+
// Global configuration
|
|
935
|
+
const client = new AstrologyApiClient({ apiVersion: 'v1' });
|
|
936
|
+
|
|
937
|
+
// Explicit override per call (via Proxy detection)
|
|
938
|
+
await client.v2.dasha.getDashaPredictiveAnalysis({ ... });
|
|
939
|
+
// Triggers POST /api/v2/dasha/getDashaPredictiveAnalysis
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
### B. Compatibility Mapping (Shim Layer)
|
|
943
|
+
If a backend method name changes (e.g., `calculateDasha` becomes `getDashaAnalysis`), the client includes a local mapping to prevent breaking old frontend code.
|
|
944
|
+
|
|
945
|
+
```javascript
|
|
946
|
+
// src/config.js logic
|
|
947
|
+
const COMPATIBILITY_MAP = {
|
|
948
|
+
'dasha.calculateDasha': 'dasha.getDashaPredictiveAnalysis',
|
|
949
|
+
'birthchart.getSummary': 'astrologer.interpretBirthChart'
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
// Internal Orchestration
|
|
953
|
+
_resolveMethod(service, method) {
|
|
954
|
+
const mapped = COMPATIBILITY_MAP[`${service}.${method}`];
|
|
955
|
+
if (mapped) {
|
|
956
|
+
const [newService, newMethod] = mapped.split('.');
|
|
957
|
+
return { service: newService, method: newMethod };
|
|
958
|
+
}
|
|
959
|
+
return { service, method };
|
|
960
|
+
}
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
## 3.1 Complete API Client Implementation with Backend Reality
|
|
964
|
+
|
|
965
|
+
```javascript
|
|
966
|
+
// packages/api-client/src/AstrologyApiClient.js
|
|
967
|
+
// Final Production Implementation satisfying Section 3 and 4 requirements
|
|
968
|
+
const { v4: uuid } = require('uuid');
|
|
969
|
+
|
|
970
|
+
class AstrologyApiClient {
|
|
971
|
+
constructor(config = {}) {
|
|
972
|
+
this.baseUrl = config.baseUrl || 'http://localhost:10000';
|
|
973
|
+
this.config = config;
|
|
974
|
+
|
|
975
|
+
// Proxy logic as defined in Section 3
|
|
976
|
+
return new Proxy(this, {
|
|
977
|
+
get: (target, serviceName) => {
|
|
978
|
+
if (serviceName in target) return target[serviceName];
|
|
979
|
+
return new Proxy({}, {
|
|
980
|
+
get: (_, methodName) => {
|
|
981
|
+
return async (params = {}) => target.call(serviceName, methodName, params);
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
asUser(platformUserId) {
|
|
989
|
+
let unifiedUserId = null;
|
|
990
|
+
let currentSessionId = null;
|
|
991
|
+
|
|
992
|
+
const ensureIdentity = async () => {
|
|
993
|
+
if (!unifiedUserId) {
|
|
994
|
+
// Reality: userMapping/mapPlatformToUnified (per Client Integration Guide)
|
|
995
|
+
const response = await fetch(`${this.baseUrl}/api/v1/userMapping/mapPlatformToUnified`, {
|
|
996
|
+
method: 'POST',
|
|
997
|
+
headers: { 'Content-Type': 'application/json' },
|
|
998
|
+
body: JSON.stringify({
|
|
999
|
+
platform: this.config.platform,
|
|
1000
|
+
platformUserId,
|
|
1001
|
+
requestId: uuid(),
|
|
1002
|
+
timestamp: new Date().toISOString(),
|
|
1003
|
+
client: {
|
|
1004
|
+
type: this.config.clientType || 'api-client',
|
|
1005
|
+
version: this.config.clientVersion || '1.0.0'
|
|
1006
|
+
},
|
|
1007
|
+
// Align with integration guide: include all required fields
|
|
1008
|
+
initialProfileData: {}
|
|
1009
|
+
})
|
|
1010
|
+
});
|
|
1011
|
+
const result = await response.json();
|
|
1012
|
+
if (result.status === 'error') throw new Error(result.error.message);
|
|
1013
|
+
unifiedUserId = result.data.unifiedUserId;
|
|
1014
|
+
// Update session ID if provided in response
|
|
1015
|
+
if (result.data.sessionId) {
|
|
1016
|
+
currentSessionId = result.data.sessionId;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return unifiedUserId;
|
|
1020
|
+
};
|
|
1021
|
+
|
|
1022
|
+
return {
|
|
1023
|
+
// Internal getters for consistent request payloads
|
|
1024
|
+
_getUnifiedUserId: () => unifiedUserId,
|
|
1025
|
+
_getPlatformUserId: () => platformUserId,
|
|
1026
|
+
|
|
1027
|
+
// Observability Interface: Direct routing to Backend ObservabilityService
|
|
1028
|
+
observability: {
|
|
1029
|
+
async incrementCounter(name, tags = {}) {
|
|
1030
|
+
const userId = await ensureIdentity();
|
|
1031
|
+
// Use the proxy architecture to call the backend observability service
|
|
1032
|
+
return await fetch(`${this.baseUrl}/api/v1/observability/recordMetric`, {
|
|
1033
|
+
method: 'POST',
|
|
1034
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1035
|
+
body: JSON.stringify({
|
|
1036
|
+
unifiedUserId: userId,
|
|
1037
|
+
type: 'counter',
|
|
1038
|
+
name,
|
|
1039
|
+
tags: { ...tags, platform: this.config.platform },
|
|
1040
|
+
requestId: uuid(),
|
|
1041
|
+
timestamp: new Date().toISOString(),
|
|
1042
|
+
client: {
|
|
1043
|
+
type: this.config.clientType || 'api-client',
|
|
1044
|
+
version: this.config.clientVersion || '1.0.0'
|
|
1045
|
+
}
|
|
1046
|
+
})
|
|
1047
|
+
});
|
|
1048
|
+
},
|
|
1049
|
+
|
|
1050
|
+
async recordHistogram(name, value, tags = {}) {
|
|
1051
|
+
const userId = await ensureIdentity();
|
|
1052
|
+
// Use the proxy architecture to call the backend observability service
|
|
1053
|
+
return await fetch(`${this.baseUrl}/api/v1/observability/recordMetric`, {
|
|
1054
|
+
method: 'POST',
|
|
1055
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1056
|
+
body: JSON.stringify({
|
|
1057
|
+
unifiedUserId: userId,
|
|
1058
|
+
type: 'histogram',
|
|
1059
|
+
name,
|
|
1060
|
+
value,
|
|
1061
|
+
tags: { ...tags, platform: this.config.platform },
|
|
1062
|
+
requestId: uuid(),
|
|
1063
|
+
timestamp: new Date().toISOString(),
|
|
1064
|
+
client: {
|
|
1065
|
+
type: this.config.clientType || 'api-client',
|
|
1066
|
+
version: this.config.clientVersion || '1.0.0'
|
|
1067
|
+
}
|
|
1068
|
+
})
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
},
|
|
1072
|
+
|
|
1073
|
+
// Session access
|
|
1074
|
+
async getSession() {
|
|
1075
|
+
const userId = await ensureIdentity();
|
|
1076
|
+
// Reality: sessionManager/getSessionState (aligned with integration guide)
|
|
1077
|
+
const response = await fetch(`${this.baseUrl}/api/v1/sessionManager/getSessionState`, {
|
|
1078
|
+
method: 'POST',
|
|
1079
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1080
|
+
body: JSON.stringify({
|
|
1081
|
+
unifiedUserId: userId,
|
|
1082
|
+
requestId: uuid(),
|
|
1083
|
+
timestamp: new Date().toISOString(),
|
|
1084
|
+
client: {
|
|
1085
|
+
type: this.config.clientType || 'api-client',
|
|
1086
|
+
version: this.config.clientVersion || '1.0.0'
|
|
1087
|
+
}
|
|
1088
|
+
})
|
|
1089
|
+
});
|
|
1090
|
+
const result = await response.json();
|
|
1091
|
+
if (result.status === 'error') throw new Error(result.error.message);
|
|
1092
|
+
currentSessionId = result.data.sessionId;
|
|
1093
|
+
|
|
1094
|
+
// Smart Unwrapping: Attach metadata as non-enumerable property
|
|
1095
|
+
if (result.data && typeof result.data === 'object') {
|
|
1096
|
+
Object.defineProperty(result.data, 'metadata', { value: result.metadata, enumerable: false });
|
|
1097
|
+
}
|
|
1098
|
+
return result.data;
|
|
1099
|
+
},
|
|
1100
|
+
|
|
1101
|
+
// Lock management
|
|
1102
|
+
async acquireSessionLock() {
|
|
1103
|
+
const userId = await ensureIdentity();
|
|
1104
|
+
const response = await fetch(`${this.baseUrl}/api/v1/sessionManager/acquireSessionLock`, {
|
|
1105
|
+
method: 'POST',
|
|
1106
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1107
|
+
body: JSON.stringify({
|
|
1108
|
+
unifiedUserId: userId,
|
|
1109
|
+
requestId: uuid(),
|
|
1110
|
+
timestamp: new Date().toISOString(),
|
|
1111
|
+
client: this.config.client || 'api-client'
|
|
1112
|
+
})
|
|
1113
|
+
});
|
|
1114
|
+
const result = await response.json();
|
|
1115
|
+
if (result.status === 'error') throw new Error(result.error.message);
|
|
1116
|
+
return true;
|
|
1117
|
+
},
|
|
1118
|
+
|
|
1119
|
+
async releaseSessionLock() {
|
|
1120
|
+
const userId = await ensureIdentity();
|
|
1121
|
+
const response = await fetch(`${this.baseUrl}/api/v1/sessionManager/releaseSessionLock`, {
|
|
1122
|
+
method: 'POST',
|
|
1123
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1124
|
+
body: JSON.stringify({
|
|
1125
|
+
unifiedUserId: userId,
|
|
1126
|
+
requestId: uuid(),
|
|
1127
|
+
timestamp: new Date().toISOString(),
|
|
1128
|
+
client: this.config.client || 'api-client'
|
|
1129
|
+
})
|
|
1130
|
+
});
|
|
1131
|
+
const result = await response.json();
|
|
1132
|
+
if (result.status === 'error') throw new Error(result.error.message);
|
|
1133
|
+
return true;
|
|
1134
|
+
},
|
|
1135
|
+
|
|
1136
|
+
// Session saving
|
|
1137
|
+
async save(partialState) {
|
|
1138
|
+
const userId = await ensureIdentity();
|
|
1139
|
+
const response = await fetch(`${this.baseUrl}/api/v1/sessionManager/updateSessionState`, {
|
|
1140
|
+
method: 'POST',
|
|
1141
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1142
|
+
body: JSON.stringify({
|
|
1143
|
+
unifiedUserId: userId,
|
|
1144
|
+
sessionId: currentSessionId,
|
|
1145
|
+
partialState,
|
|
1146
|
+
requestId: uuid(),
|
|
1147
|
+
timestamp: new Date().toISOString(),
|
|
1148
|
+
client: this.config.client || 'api-client'
|
|
1149
|
+
})
|
|
1150
|
+
});
|
|
1151
|
+
const result = await response.json();
|
|
1152
|
+
if (result.status === 'error') throw new Error(result.error.message);
|
|
1153
|
+
|
|
1154
|
+
// Smart Unwrapping: Attach metadata as non-enumerable property
|
|
1155
|
+
if (result.data && typeof result.data === 'object') {
|
|
1156
|
+
Object.defineProperty(result.data, 'metadata', { value: result.metadata, enumerable: false });
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Auto-release lock after save
|
|
1160
|
+
await fetch(`${this.baseUrl}/api/v1/sessionManager/releaseSessionLock`, {
|
|
1161
|
+
method: 'POST',
|
|
1162
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1163
|
+
body: JSON.stringify({
|
|
1164
|
+
unifiedUserId: userId,
|
|
1165
|
+
requestId: uuid(),
|
|
1166
|
+
timestamp: new Date().toISOString(),
|
|
1167
|
+
client: this.config.client || 'api-client'
|
|
1168
|
+
})
|
|
1169
|
+
});
|
|
1170
|
+
return result.data;
|
|
1171
|
+
},
|
|
1172
|
+
|
|
1173
|
+
// Service calls
|
|
1174
|
+
async call(service, method, params = {}) {
|
|
1175
|
+
const userId = await ensureIdentity();
|
|
1176
|
+
// Reality: direct routing /api/v1/:service/:method with comprehensive platform context
|
|
1177
|
+
// Align with integration guide schema specifications
|
|
1178
|
+
const response = await fetch(`${this.baseUrl}/api/v1/${service}/${method}`, {
|
|
1179
|
+
method: 'POST',
|
|
1180
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1181
|
+
body: JSON.stringify({
|
|
1182
|
+
// Top-level schema requirements from integration guide
|
|
1183
|
+
requestId: uuid(), // Required: UUID for tracing
|
|
1184
|
+
timestamp: new Date().toISOString(), // Required: ISO-8601 timestamp
|
|
1185
|
+
platform: this.config.platform, // Required: enum [whatsapp, web, ios, android, pwa]
|
|
1186
|
+
platformUserId, // Required: platform-specific user ID
|
|
1187
|
+
client: this.config.client || { type: this.config.platform, version: '1.0' }, // Required: client object
|
|
1188
|
+
unifiedUserId: userId, // Optional*: Required for fast path after registration
|
|
1189
|
+
sessionId: this.currentSessionId, // Optional*: Required for continuity within journey
|
|
1190
|
+
// Include additional context fields required by backend
|
|
1191
|
+
initialProfileData: params.initialProfileData || {},
|
|
1192
|
+
preferences: params.preferences || {},
|
|
1193
|
+
parameters: params.parameters || {}, // Calculation-specific arguments
|
|
1194
|
+
...params
|
|
1195
|
+
})
|
|
1196
|
+
});
|
|
1197
|
+
const result = await response.json();
|
|
1198
|
+
if (result.status === 'error') throw new Error(result.error.message);
|
|
1199
|
+
|
|
1200
|
+
// Smart Unwrapping: Hand clean data but attach metadata as non-enumerable property for tracing
|
|
1201
|
+
if (result.data && typeof result.data === 'object') {
|
|
1202
|
+
Object.defineProperty(result.data, 'metadata', {
|
|
1203
|
+
value: result.metadata,
|
|
1204
|
+
enumerable: false
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
return result.data;
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
module.exports = AstrologyApiClient;
|
|
1214
|
+
```
|
|
1215
|
+
|
|
1216
|
+
### Backend API Reality Integration
|
|
1217
|
+
|
|
1218
|
+
The implementation includes several key enhancements that reflect the actual backend implementation:
|
|
1219
|
+
|
|
1220
|
+
**Session Management Enhancements:**
|
|
1221
|
+
- **Auto-creation of sessions**: The backend automatically creates sessions if they don't exist, rather than failing
|
|
1222
|
+
- **Enhanced `getSessionState()` method**: Now auto-creates sessions if not found, ensuring continuity
|
|
1223
|
+
- **Improved error recovery**: More robust fallback mechanisms when sessions aren't found
|
|
1224
|
+
|
|
1225
|
+
**Identity Resolution Improvements:**
|
|
1226
|
+
- **Deterministic unified ID format**: Changed from `unified_{timestamp}_{platform_abbrev}_{random_hash}` to `unified_{platformAbbr}_{identityHash}` for consistency
|
|
1227
|
+
- **Enhanced phone canonicalization**: Better validation and error handling for phone numbers
|
|
1228
|
+
- **Improved fallback mechanisms**: Better handling when user mapping fails
|
|
1229
|
+
|
|
1230
|
+
**Schema Validation Updates:**
|
|
1231
|
+
- **ServiceRegistry integration**: Automatic schema inference based on service/method names
|
|
1232
|
+
- **Enhanced validation middleware**: More sophisticated validation with custom mappings
|
|
1233
|
+
- **Better error responses**: More detailed validation error messages
|
|
1234
|
+
|
|
1235
|
+
**Error Handling Improvements:**
|
|
1236
|
+
- **Comprehensive custom error classes**: Detailed AstrologyError subclasses with proper HTTP status codes
|
|
1237
|
+
- **Better error response format**: More consistent responses with proper metadata
|
|
1238
|
+
- **Enhanced observability**: Better integration with observability services
|
|
1239
|
+
|
|
1240
|
+
**API Structure:**
|
|
1241
|
+
- **Method-based routing**: Fully implemented at `/api/v1/:service/:method`
|
|
1242
|
+
- **Health checks**: Comprehensive health check endpoints at `/api/v1/health`
|
|
1243
|
+
- **Documentation endpoints**: Available API documentation at `/api/v1/docs`
|
|
1244
|
+
- **Service discovery**: Available at `/api/v1/:service/methods` and `/api/v1/:service/methods/:method`
|
|
1245
|
+
|
|
1246
|
+
### Request Envelope Schema Compliance
|
|
1247
|
+
|
|
1248
|
+
The API client ensures all requests comply with the integration guide's schema specifications:
|
|
1249
|
+
|
|
1250
|
+
**Top-Level Schema Requirements:**
|
|
1251
|
+
| Field | Type | Required | Format | Description |
|
|
1252
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
1253
|
+
| `requestId` | String | **YES** | UUID | Must be unique per call. |
|
|
1254
|
+
| `timestamp` | String | **YES** | date-time | Client-side ISO-8601 string. |
|
|
1255
|
+
| `platform` | String | **YES** | enum | `whatsapp`, `web`, `ios`, `android`, `pwa`. |
|
|
1256
|
+
| `platformUserId` | String | **YES** | string | The ID from the platform. |
|
|
1257
|
+
| `client` | Object | **YES** | object | `{ "type": "ios", "version": "1.0" }`. |
|
|
1258
|
+
| `unifiedUserId` | String | No* | string | Required for Fast Path after registration. |
|
|
1259
|
+
| `sessionId` | String | No* | string | Required for continuity within a journey. |
|
|
1260
|
+
| `birthData` | Object | No* | object | Required for first calculation if not registered. |
|
|
1261
|
+
| `initialProfileData`| Object | No | object | Use for any profile updates or seeding. |
|
|
1262
|
+
| `parameters` | Object | No | object | Calculation-specific arguments. |
|
|
1263
|
+
|
|
1264
|
+
**Birth Data Schema (Calculation Input):**
|
|
1265
|
+
| Field | Type | Required | Pattern/Format | Description |
|
|
1266
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
1267
|
+
| `date` | String | **YES** | `DD/MM/YYYY` | Must include leading zeros (e.g., `05/01/1990`). |
|
|
1268
|
+
| `time` | String | **YES** | `HH:MM` | 24-hour format (e.g., `14:30`). No seconds. |
|
|
1269
|
+
| `place` | String | **YES** | Min 3 chars | Name of the city/country. |
|
|
1270
|
+
| `coordinates` | Object | No | object | `{ "latitude": 19.0, "longitude": 72.8 }`. |
|
|
1271
|
+
| `timezone` | String | No | string | IANA string (e.g., `Asia/Kolkata`). |
|
|
1272
|
+
|
|
1273
|
+
**Profile Data Schema (Update/Seeding):**
|
|
1274
|
+
| Field | Type | Description |
|
|
1275
|
+
| :--- | :--- | :--- |
|
|
1276
|
+
| `firstName` | String | User's first name. |
|
|
1277
|
+
| `lastName` | String | User's family name. |
|
|
1278
|
+
| `email` | String | Valid email address. |
|
|
1279
|
+
| `phone` | String | E.164 format phone number (e.g., `+1...`). |
|
|
1280
|
+
---
|
|
1281
|
+
|
|
1282
|
+
## 14. Microservices & Gateway Routing (Future-Proofing)
|
|
1283
|
+
|
|
1284
|
+
The client is designed to handle the transition from a bundled backend to a debundled microservices architecture without requiring changes to the frontend business logic.
|
|
1285
|
+
|
|
1286
|
+
### Scenario A: The API Gateway (Default)
|
|
1287
|
+
In this scenario, all microservices are routed through a single entry point (e.g., Nginx or Cloudflare).
|
|
1288
|
+
* **Routing:** The gateway handles `/dasha/*` vs `/ai/*`.
|
|
1289
|
+
* **Client Status:** **Zero changes needed.** The client continues to use a single `baseUrl`.
|
|
1290
|
+
|
|
1291
|
+
### Scenario B: Distributed Microservices
|
|
1292
|
+
In this scenario, every service has its own public URL (e.g., `dasha.api.com`). The client is "Discovery Ready" via its Proxy pattern.
|
|
1293
|
+
|
|
1294
|
+
```javascript
|
|
1295
|
+
// Future Configuration for Distributed Services
|
|
1296
|
+
const client = new AstrologyApiClient({
|
|
1297
|
+
baseUrl: 'https://gateway.api.com/api/v1',
|
|
1298
|
+
services: {
|
|
1299
|
+
dasha: 'https://dasha-service.internal/api/v1',
|
|
1300
|
+
ai: 'https://ai-service.internal/api/v1',
|
|
1301
|
+
birthchart: 'https://calculation-cluster.internal/api/v1'
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
// Internal Proxy Selection Logic
|
|
1306
|
+
_resolveUrl(serviceName) {
|
|
1307
|
+
// If a specific URL is mapped for the service, use it; otherwise fallback to baseUrl.
|
|
1308
|
+
return this.config.services[serviceName] || this.config.baseUrl;
|
|
1309
|
+
}
|
|
1310
|
+
```
|
|
1311
|
+
|
|
1312
|
+
### The "Insurance Policy"
|
|
1313
|
+
Because your frontend code always uses `client.serviceName.methodName()`, we can move `dasha` from the main server to its own cluster tomorrow, update the `services` map in the client config once, and your WhatsApp/iOS apps will keep working perfectly without a single line of their code being modified.
|