docs: update README files for core packages with architecture diagrams and usage examples

This commit is contained in:
Firman Ramdhani
2026-05-28 12:20:53 +07:00
parent df229c9984
commit c510feadbb
4 changed files with 255 additions and 87 deletions
+68 -54
View File
@@ -1,65 +1,79 @@
# @repo/core-api
# Enterprise API Engine (`@repo/core-api`)
The platform-agnostic API engine for the monorepo. Provides an isolated HTTP client factory, a unified observability pipeline (Grafana Faro + OpenTelemetry), and a generic data services engine — consumed by `apps/web`, `apps/landing`, and any future workspace.
[← Back to Root](../../README.md)
---
The platform-agnostic API engine for the monorepo. Provides an isolated HTTP client factory, a unified observability pipeline (Grafana Faro + OpenTelemetry), and a generic data services engine.
## Table of Contents
- [Architecture Overview](#architecture-overview)
- [HTTP Client](#http-client)
- [Observability](#observability)
- [Data Services](#data-services)
- [Application Setup Guide](#application-setup-guide)
- [Per-Request Telemetry (Escape Hatch)](#per-request-telemetry-escape-hatch)
- [Error Handling](#error-handling)
- [Package Exports](#package-exports)
**This package enforces App Autonomy (IoC).** The core provides the engine and interceptor pipelines, but the consuming applications (`apps/web`, `apps/landing`) inject their own specific configurations, authentication tokens, and error handling behaviors.
---
## Architecture Overview
```
┌────────────────────────────────────────────────────────────────────┐
│ @repo/core-api │
│ │
│ ┌──────────────┐ ┌───────────────────┐ ┌───────────────────┐ │
│ │ http-client │ │ observability │ │ data-services │ │
│ │ │ │ │ │ │ │
│ │ createHttp │◄──│ faroAdapter │ │ BaseRemoteData │ │
│ │ Client() │ │ initTelemetry() │ │ Services │ │
│ │ │ │ getFaro() │ │ CommonRemoteData │ │
│ │ ApiResponse │ │ noopAdapter │ │ Services │ │
│ └──────┬───────┘ └───────────────────┘ └────────┬──────────┘ │
│ │ │ │
│ └────────────────────┬───────────────────────┘ │
│ │ │
│ ┌────────┴────────┐ │
│ │ errors │ │
│ │ ApiError │ │
│ │ ErrorCodes │ │
│ └─────────────────┘ │
└────────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
apps/web apps/landing apps/desktop
```mermaid
graph TD
subgraph Apps ["apps/* (App Autonomy)"]
WEB[apps/web]
LAND[apps/landing]
DESK[apps/desktop]
end
subgraph Core ["@repo/core-api (Engine)"]
subgraph HTTP ["http-client"]
FACTORY[createHttpClient]
end
subgraph OBS ["observability"]
FARO[faroAdapter]
end
subgraph DATA ["data-services"]
BASE[BaseRemoteDataServices]
COMMON[CommonRemoteDataServices]
end
subgraph ERRORS ["errors"]
API_ERR[ApiError]
end
end
WEB & LAND & DESK -->|instantiates| FACTORY
WEB & LAND & DESK -->|extends| COMMON
COMMON -->|executes via| FACTORY
FACTORY -.->|reports via| FARO
FACTORY -.->|throws| API_ERR
style Core fill:#f8f9fa,stroke:#ced4da
style Apps fill:#e9ecef,stroke:#adb5bd
```
### Data Flow
### Data Flow Lifecycle
Every HTTP request flows through this pipeline:
Every HTTP request flows through this precise interceptor pipeline:
```
Component → DataService.getMany() → execute()
→ httpClient.request()
→ Request Interceptor:
1. faroAdapter.onRequestStart() ← Faro log + optional custom span
2. hooks.onRequest() ← App-specific (e.g., Bearer token)
→ Network (fetch/XHR)
→ Response Interceptor:
SUCCESS: faroAdapter.onRequestEnd() → hooks.onResponse()
ERROR: faroAdapter.onRequestError() → hooks.onResponseError()
→ ApiError.fromAxiosError()
```mermaid
sequenceDiagram
participant C as UI Component
participant S as Data Service
participant H as HTTP Client
participant F as Faro Adapter
participant A as App Hooks (IoC)
participant N as Network
C->>S: getMany()
S->>H: request()
H->>F: onRequestStart() (Log + Span)
H->>A: hooks.onRequest() (Inject Token)
A->>N: fetch/XHR
alt Success
N-->>A: 200 OK
A->>F: onRequestEnd() (Close Span)
F->>A: hooks.onResponse()
A-->>S: return data
else Error
N-->>A: 401 / 500
A->>F: onRequestError() (Log Error)
F->>A: hooks.onResponseError() (Redirect/Refresh)
A-->>S: throw ApiError
end
```
> [!IMPORTANT]
@@ -143,10 +157,10 @@ import { initTelemetry } from '@repo/core-api/observability/setup';
initTelemetry({
appName: 'fe-monorepo-web',
appVersion: '1.0.0',
telemetryUrl: 'https://telemetry.eigen.co.id/collect',
telemetryUrl: '[https://telemetry.eigen.co.id/collect](https://telemetry.eigen.co.id/collect)',
environment: 'production',
// Optional: direct OTLP export to Grafana Tempo
otlpTraceUrl: 'https://telemetry.eigen.co.id/v1/traces',
otlpTraceUrl: '[https://telemetry.eigen.co.id/v1/traces](https://telemetry.eigen.co.id/v1/traces)',
});
```
@@ -254,8 +268,8 @@ import { initTelemetry } from '@repo/core-api/observability/setup';
initTelemetry({
appName: import.meta.env.VITE_APP_NAME || 'fe-monorepo-web',
appVersion: import.meta.env.VITE_APP_VERSION || '0.0.0',
telemetryUrl: import.meta.env.VITE_FARO_URL || 'https://telemetry.eigen.co.id/collect',
otlpTraceUrl: import.meta.env.VITE_OTLP_TRACE_URL || 'https://telemetry.eigen.co.id/v1/traces',
telemetryUrl: import.meta.env.VITE_FARO_URL || '[https://telemetry.eigen.co.id/collect](https://telemetry.eigen.co.id/collect)',
otlpTraceUrl: import.meta.env.VITE_OTLP_TRACE_URL || '[https://telemetry.eigen.co.id/v1/traces](https://telemetry.eigen.co.id/v1/traces)',
environment: import.meta.env.VITE_ENV || 'development',
});
+40 -6
View File
@@ -89,6 +89,12 @@ declare module '@repo/core-events' {
'STORE:ORDER_PLACED': OrderPayload;
'STORE:ORDER_CANCELLED': { orderId: string; reason: string };
'UI:SIDEBAR_TOGGLED': { collapsed: boolean };
// Explicit payloads for the examples below:
'DEVICE:PRINT_RECEIPT': { receiptId: string; items: any[]; total: number; cashierName: string; timestamp: number };
'WS:STOCK_UPDATE': { id: string; price: number };
'AUTH:PROFILE_UPDATED': { id: string; name: string; email: string; avatar: string; updatedAt: number };
'SYSTEM:ERROR': { source: string; error: Error };
}
}
```
@@ -102,7 +108,7 @@ function CheckoutButton() {
const publish = usePublishEvent();
// ✅ 'STORE:ORDER_PLACED' autocompletes.
// ✅ Payload shape is enforced by TypeScript.
publish('STORE:ORDER_PLACED', { orderId: '123', total: 99, items: [...] });
publish('STORE:ORDER_PLACED', { orderId: '123', total: 99, items: [] });
}
function OrderTracker() {
@@ -124,6 +130,29 @@ function OrderTracker() {
---
## Usage Outside React (Vanilla TS)
For utility files, API interceptors, Web Workers, or vanilla functions where React hooks cannot be used, import the raw `eventBus` instance directly.
```ts
import { eventBus } from '@repo/core-events';
// Publishing
eventBus.publish('STORE:ORDER_CANCELLED', { orderId: '123', reason: 'Out of stock' });
// Subscribing
const handler = (payload) => {
console.log('Order cancelled:', payload.orderId);
};
eventBus.subscribe('STORE:ORDER_CANCELLED', handler);
// CRITICAL: Always unsubscribe when done to prevent memory leaks in non-React contexts!
eventBus.unsubscribe('STORE:ORDER_CANCELLED', handler);
```
---
## Usage Examples
Here are three real-world architectural patterns powered by the Event Bus. All event types below are registered in `apps/web/src/types/events.d.ts`, **not** in the core package.
@@ -145,7 +174,7 @@ export function CashierUI() {
// Fire and forget. Zero knowledge of how printing actually happens.
publish('DEVICE:PRINT_RECEIPT', {
receiptId: 'RCP-123',
items: [...],
items: [],
total: 45.00,
cashierName: 'Firman',
timestamp: Date.now(),
@@ -235,7 +264,7 @@ export const StockRow = memo(function StockRow({ stockId }) {
**Problem**: When a user updates their profile, we need to persist it to the secure local IndexedDB. We don't want to tightly couple our UI forms to the `@repo/core-storage` package.
**Solution**: The UI form announces the profile update. A dedicated storage listener persists it in the background.
**Solution**: The UI form announces the profile update. A dedicated storage listener persists it in the background, properly escalating errors if the storage fails.
**Publisher (Profile UI)**:
```tsx
@@ -249,7 +278,7 @@ export function ProfileSettingsUI() {
id: 'user-1',
name: 'Firman',
email: 'firman@eigen.co.id',
avatar: 'https://example.com/avatar.png',
avatar: '[https://example.com/avatar.png](https://example.com/avatar.png)',
updatedAt: Date.now(),
});
};
@@ -260,14 +289,19 @@ export function ProfileSettingsUI() {
**Subscriber (Storage Sync Listener)**:
```tsx
import { useAppEvent } from '@repo/core-events';
import { useAppEvent, usePublishEvent } from '@repo/core-events';
import { secureIndexedDB } from '@repo/core-storage';
export function StorageSyncListener() {
const publish = usePublishEvent();
useAppEvent('AUTH:PROFILE_UPDATED', (payload) => {
// Automatically encrypted at rest because 'user_profile'
// is defined in ENCRYPTED_KEYS in @repo/core-storage
secureIndexedDB.setItem('user_profile', payload).catch(console.error);
secureIndexedDB.setItem('user_profile', payload).catch((error) => {
// Escalate to global error handler instead of swallowing it
publish('SYSTEM:ERROR', { source: 'StorageSyncListener', error });
});
});
return null;
+70 -2
View File
@@ -1,5 +1,7 @@
# Enterprise i18n Architecture (`@repo/core-i18n`)
[← Back to Root](../../README.md)
A highly decoupled, type-safe internationalization engine for the Eigen Monorepo.
It uses a **Hybrid Namespace Strategy**:
@@ -10,6 +12,43 @@ This architecture strictly adheres to **Inversion of Control (IoC)**. The core e
---
## Overview Architecture
```mermaid
graph TD
subgraph Apps ["apps/* (App Autonomy)"]
UI[React Components]
DICT[Feature Dictionaries<br/>e.g., booking.json]
end
subgraph Core ["@repo/core-i18n (Engine)"]
I18N((i18next Instance))
STORE[(core-storage)]
COMMON[Common Vocabulary]
end
subgraph Backend ["Backend API"]
SYNC[Language Sync Endpoint]
TENANT[Tenant Config Endpoint]
end
UI -->|uses useTranslation| I18N
DICT -.->|lazy loads| I18N
COMMON -->|preloads| I18N
I18N <-->|reads/persists| STORE
I18N -->|changeLanguage sync| SYNC
SYNC -.->|fails? rollback| I18N
TENANT -.->|applyTenantOverrides| I18N
style I18N fill:#4263eb,color:#fff,stroke:#fff
style Apps fill:#f8f9fa,stroke:#ced4da
style Core fill:#f8f9fa,stroke:#ced4da
```
---
## 1. App-Level Setup (Bootstrap)
Initialize the engine *before* your React application mounts to prevent UI flashing.
@@ -84,9 +123,38 @@ export default function BookingFeature() {
}
```
**3. Dynamic Variables (Interpolation):**
```json
// booking.json
{
"messages": {
"welcome": "Welcome back, {{name}}! You have {{count}} new bookings."
}
}
```
```tsx
// Inside component
<h1>{t('booking:messages.welcome', { name: 'Firman', count: 5 })}</h1>
```
---
## 3. Real-World Implementation Flow
## 3. Usage Outside React Components (Vanilla TS)
For utility files, API interceptors, or vanilla functions where React hooks cannot be used, import the raw `i18n` instance directly.
```ts
import { i18n } from '@repo/core-i18n';
// Must specify the namespace explicitly if it's not 'common'
export const getErrorMessage = (code: string) => {
return i18n.t(`booking:errors.${code}`, { defaultValue: 'Unknown Error' });
};
```
---
## 4. Real-World Implementation Flow
The engine supports robust flows for authenticated apps, including Tenant Vocabulary Overrides and Backend Synchronization.
@@ -151,7 +219,7 @@ const handleSwitch = async (newLng: string) => {
---
## 4. Backend API Contract (For Backend Engineers)
## 5. Backend API Contract (For Backend Engineers)
To support Dynamic Tenant Overrides, the backend must expose an endpoint (e.g., `GET /v1/tenant/i18n-config`).
+74 -22
View File
@@ -1,4 +1,6 @@
# @repo/core-storage
# Enterprise Storage Engine (`@repo/core-storage`)
[← Back to Root](../../README.md)
The **Enterprise-grade storage engine** for the monorepo.
@@ -6,33 +8,61 @@ This package provides a unified, Promise-based interface for interacting with br
---
## Architecture & Data Flow
```mermaid
graph TD
subgraph Apps ["apps/* (Consumers)"]
UI[React Components / API Interceptors]
end
subgraph Core ["@repo/core-storage (Engine)"]
API[IStorageService API]
REG[[StorageKey & ENCRYPTED_KEYS Registry]]
ENC{{AES Encryption Pipeline}}
LOCAL[LocalStorage Adapter]
IDB[IndexedDB Adapter]
end
subgraph Browser ["Browser APIs"]
B_LOCAL[(localStorage)]
B_IDB[(IndexedDB)]
end
UI -->|getItem / setItem| API
API --> REG
REG -.->|Sensitive Key?| ENC
ENC -.-> LOCAL & IDB
REG -.->|Plain-text Key| LOCAL & IDB
LOCAL <--> B_LOCAL
IDB <--> B_IDB
style Core fill:#f8f9fa,stroke:#ced4da
style ENC fill:#fab005,color:#fff,stroke:#fff
style Browser fill:#e9ecef,stroke:#adb5bd
```
---
## 🎯 Primary Goals & Separation of Concerns
* **Separation from `@repo/core-api`**: Storage is a fundamental primitive. While the API client uses storage (to retrieve tokens), storage itself does not need to know about HTTP requests.
* **Dual Backend Strategy**:
* `secureStorage` (localStorage): Ideal for small, synchronous-like data (tokens, user preferences).
* `IndexedDBService`: Built for large, asynchronous data (offline drafts, cached API responses, blobs) without the 5MB size limit.
* `secureStorage` (localStorage): Ideal for small, synchronous-like data (tokens, user preferences, settings).
* `secureIndexedDB` (IndexedDB): Built for large, asynchronous data (offline drafts, cached API responses, large arrays/blobs) bypassing the 5MB localStorage limit.
* **Security by Default**: Developers don't need to manually encrypt/decrypt data. If a key is marked as sensitive, the library handles AES encryption transparently.
---
## ✨ Key Features
| Feature | Description |
|---|---|
| 🔒 **Selective Encryption** | Uses `@repo/utils` `EncryptionUtils` to automatically AES-encrypt payloads whose keys are listed in `ENCRYPTED_KEYS`. |
| 🛡️ **Type-Safe Keys** | All keys must be registered in `storage.key.ts`. Prevents typos and key collisions across the monorepo. |
| 🔄 **Unified Promise API** | Both `localStorage` and `IndexedDB` implement the same async `IStorageService` interface. |
| 🧬 **Strict Generics** | Read and write operations enforce payload types via generics (e.g., `getItem<UserProfile>('user_profile')`). |
| 🩹 **Corrupt Data Resilience** | If parsing or decryption fails (e.g., tampered data), the corrupt entry is safely removed and returns `null`. |
* **Corrupt Data Resilience**: If parsing or decryption fails (e.g., tampered data or changed encryption keys), the corrupt entry is safely removed and returns `null`, preventing the app from crashing.
---
## 🚀 Usage Examples
Because this package is framework-agnostic, these instances can be imported anywhere: React components, Redux/Zustand stores, or Axios interceptors.
### 1. Secure Local Storage (Tokens, Profile)
Use `secureStorage` (or your app's configured instance) for small payloads. If the key is in `ENCRYPTED_KEYS`, it will be encrypted at rest.
Use `secureStorage` for small payloads. If the key is in `ENCRYPTED_KEYS`, it will be encrypted at rest automatically.
```typescript
import { secureStorage, StorageKey } from '@repo/core-storage';
@@ -46,7 +76,7 @@ await secureStorage.setItem(StorageKey.USER_PROFILE, {
role: 'admin'
});
// READ
// READ (Returns null if not found or if decryption fails)
const profile = await secureStorage.getItem<UserProfile>(StorageKey.USER_PROFILE);
if (profile) {
console.log('Welcome back,', profile.name);
@@ -58,7 +88,7 @@ await secureStorage.removeItem(StorageKey.USER_PROFILE);
### 2. IndexedDB (Offline Data, Large Payloads)
Use the pre-configured `secureIndexedDB` (or instantiate your own) for large, asynchronous data. It uses the exact same API and encryption pipeline.
Use the pre-configured `secureIndexedDB` for large, asynchronous data. It uses the exact same `IStorageService` interface and encryption pipeline as local storage.
```typescript
import { secureIndexedDB } from '@repo/core-storage';
@@ -69,7 +99,7 @@ interface DraftData {
lastModified: number;
}
// Save a large draft offline
// Save a large draft offline (No 5MB limit)
await secureIndexedDB.setItem('offline_draft_123', {
id: '123',
content: 'Huge text content...',
@@ -82,9 +112,9 @@ const draft = await secureIndexedDB.getItem<DraftData>('offline_draft_123');
---
## 🔑 Adding New Keys
## 🔑 Adding New Keys (Current Registry Pattern)
To maintain type safety and avoid collisions, **all** `localStorage` keys must be registered in `packages/core-storage/src/storage.key.ts`.
To maintain type safety and avoid collisions across the monorepo, **all** global storage keys must currently be registered in `packages/core-storage/src/storage.key.ts`.
### 1. Register the Key
@@ -111,4 +141,26 @@ export const ENCRYPTED_KEYS: ReadonlySet<string> = new Set<string>([
```
> [!WARNING]
> If you add an existing plain-text key to `ENCRYPTED_KEYS`, existing users will experience a parsing failure on their next session (because the library expects encrypted data but finds plain text). The resilient parser will catch this and clear the key, effectively logging them out or resetting the preference.
> **Migration Hazard**: If you add an existing plain-text key to `ENCRYPTED_KEYS`, existing users will experience a parsing failure on their next session (because the library expects encrypted data but finds plain text). The resilient parser will catch this and gracefully clear the key, which may effectively log them out or reset their local preference.
---
## 🚧 Planned Updates: Inversion of Control (IoC) Refactor
**Current Limitation:** Currently, the `StorageKey` and `ENCRYPTED_KEYS` registries live inside `@repo/core-storage`. This violates the strict **App Autonomy (IoC)** principle established in other core packages (like `@repo/core-events`), as consuming applications must modify the core package to register their app-specific keys.
**Future Architecture Roadmap:** In a future major update, this package will be refactored into a pure Factory/Engine pattern to fully decouple it from application business logic.
1. The core will export a generic `StorageEngine` class or `createStorage()` factory.
2. Apps (`apps/web`, `apps/landing`) will instantiate their own storage engines and define their own key registries and encryption rules autonomously.
*Proposed Future API:*
```typescript
// apps/web/src/lib/storage.ts
import { StorageEngine } from '@repo/core-storage';
export const webStorage = new StorageEngine({
prefix: 'web_erp_',
encryptedKeys: ['access_token', 'user_profile'],
});
```