diff --git a/packages/core-api/README.md b/packages/core-api/README.md
index d867f50..9ca1f74 100644
--- a/packages/core-api/README.md
+++ b/packages/core-api/README.md
@@ -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',
});
@@ -422,4 +436,4 @@ try {
| `@repo/core-api/observability` | `faroAdapter`, `noopObservabilityAdapter`, `IObservabilityAdapter`, `initTelemetry`, `getFaro`, `TelemetryConfig` |
| `@repo/core-api/observability/setup` | `initTelemetry`, `getFaro`, `TelemetryConfig` |
| `@repo/core-api/data-services` | `BaseRemoteDataServices`, `CommonRemoteDataServices`, types, constants |
-| `@repo/core-api/errors` | `ApiError`, `ApiErrorCode` |
+| `@repo/core-api/errors` | `ApiError`, `ApiErrorCode` |
\ No newline at end of file
diff --git a/packages/core-events/README.md b/packages/core-events/README.md
index 0c9dcc5..49e9f73 100644
--- a/packages/core-events/README.md
+++ b/packages/core-events/README.md
@@ -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,16 +289,21 @@ 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;
}
-```
+```
\ No newline at end of file
diff --git a/packages/core-i18n/README.md b/packages/core-i18n/README.md
index c34d7ba..944d53b 100644
--- a/packages/core-i18n/README.md
+++ b/packages/core-i18n/README.md
@@ -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
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
+