@sincpro/mobile 0.2.0 → 0.4.1

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.
Files changed (95) hide show
  1. package/README.md +1063 -47
  2. package/dist/entrypoints/db/migrations.d.ts +3 -1
  3. package/dist/entrypoints/db/migrations.js +65 -0
  4. package/dist/entrypoints/queue/QueueProcessor.js +1 -25
  5. package/dist/entrypoints/queue/subscriber_filter.d.ts +9 -0
  6. package/dist/entrypoints/queue/subscriber_filter.js +25 -0
  7. package/dist/entrypoints/ui/AppShell.d.ts +55 -1
  8. package/dist/entrypoints/ui/AppShell.js +50 -5
  9. package/dist/entrypoints/ui/theme.d.ts +8 -7
  10. package/dist/entrypoints/ui/theme.js +8 -7
  11. package/dist/framework/base_module.d.ts +1 -0
  12. package/dist/framework/base_module.js +6 -1
  13. package/dist/framework/createApp.d.ts +6 -0
  14. package/dist/framework/createApp.js +5 -1
  15. package/dist/index.d.ts +6 -0
  16. package/dist/index.js +3 -0
  17. package/dist/infrastructure/context_manager/context.d.ts +16 -0
  18. package/dist/infrastructure/context_manager/context.js +37 -0
  19. package/dist/infrastructure/context_manager/context_api.d.ts +28 -0
  20. package/dist/infrastructure/context_manager/context_api.js +55 -0
  21. package/dist/infrastructure/context_manager/context_key.d.ts +15 -0
  22. package/dist/infrastructure/context_manager/context_key.js +3 -0
  23. package/dist/infrastructure/context_manager/context_manager_facade.d.ts +107 -0
  24. package/dist/infrastructure/context_manager/context_manager_facade.js +113 -0
  25. package/dist/infrastructure/context_manager/decorators/propagate_context.d.ts +24 -0
  26. package/dist/infrastructure/context_manager/decorators/propagate_context.js +52 -0
  27. package/dist/infrastructure/context_manager/decorators/with_context.d.ts +34 -0
  28. package/dist/infrastructure/context_manager/decorators/with_context.js +44 -0
  29. package/dist/infrastructure/context_manager/index.d.ts +11 -0
  30. package/dist/infrastructure/context_manager/index.js +12 -0
  31. package/dist/infrastructure/context_manager/managed_context.d.ts +87 -0
  32. package/dist/infrastructure/context_manager/managed_context.js +144 -0
  33. package/dist/infrastructure/context_manager/stack_context_manager.d.ts +39 -0
  34. package/dist/infrastructure/context_manager/stack_context_manager.js +56 -0
  35. package/dist/infrastructure/database/connector.js +10 -1
  36. package/dist/infrastructure/interceptor/index.d.ts +4 -0
  37. package/dist/infrastructure/interceptor/index.js +2 -0
  38. package/dist/infrastructure/interceptor/intercept.d.ts +57 -0
  39. package/dist/infrastructure/interceptor/intercept.js +112 -0
  40. package/dist/infrastructure/interceptor/logging_hooks.d.ts +22 -0
  41. package/dist/infrastructure/interceptor/logging_hooks.js +27 -0
  42. package/dist/infrastructure/logger.d.ts +11 -16
  43. package/dist/infrastructure/logger.js +61 -18
  44. package/dist/infrastructure/telemetry/buffer_registry.d.ts +15 -0
  45. package/dist/infrastructure/telemetry/buffer_registry.js +26 -0
  46. package/dist/infrastructure/telemetry/connectivity_state.d.ts +20 -0
  47. package/dist/infrastructure/telemetry/connectivity_state.js +28 -0
  48. package/dist/infrastructure/telemetry/context_keys.d.ts +7 -0
  49. package/dist/infrastructure/telemetry/context_keys.js +7 -0
  50. package/dist/infrastructure/telemetry/fetch_with_timeout.d.ts +8 -0
  51. package/dist/infrastructure/telemetry/fetch_with_timeout.js +17 -0
  52. package/dist/infrastructure/telemetry/flush_telemetry.d.ts +40 -0
  53. package/dist/infrastructure/telemetry/flush_telemetry.js +72 -0
  54. package/dist/infrastructure/telemetry/index.d.ts +17 -0
  55. package/dist/infrastructure/telemetry/index.js +17 -0
  56. package/dist/infrastructure/telemetry/init_telemetry.d.ts +21 -0
  57. package/dist/infrastructure/telemetry/init_telemetry.js +85 -0
  58. package/dist/infrastructure/telemetry/logging/flush_job.d.ts +44 -0
  59. package/dist/infrastructure/telemetry/logging/flush_job.js +69 -0
  60. package/dist/infrastructure/telemetry/logging/log_queue_repository.d.ts +76 -0
  61. package/dist/infrastructure/telemetry/logging/log_queue_repository.js +147 -0
  62. package/dist/infrastructure/telemetry/logging/loki_client.d.ts +32 -0
  63. package/dist/infrastructure/telemetry/logging/loki_client.js +118 -0
  64. package/dist/infrastructure/telemetry/logging/loki_registry.d.ts +12 -0
  65. package/dist/infrastructure/telemetry/logging/loki_registry.js +17 -0
  66. package/dist/infrastructure/telemetry/telemetry_flush_worker.d.ts +35 -0
  67. package/dist/infrastructure/telemetry/telemetry_flush_worker.js +65 -0
  68. package/dist/infrastructure/telemetry/telemetry_signal.d.ts +18 -0
  69. package/dist/infrastructure/telemetry/telemetry_signal.js +33 -0
  70. package/dist/infrastructure/telemetry/tracing/active_span.d.ts +22 -0
  71. package/dist/infrastructure/telemetry/tracing/active_span.js +40 -0
  72. package/dist/infrastructure/telemetry/tracing/otlp_client.d.ts +13 -0
  73. package/dist/infrastructure/telemetry/tracing/otlp_client.js +34 -0
  74. package/dist/infrastructure/telemetry/tracing/otlp_serializer.d.ts +67 -0
  75. package/dist/infrastructure/telemetry/tracing/otlp_serializer.js +101 -0
  76. package/dist/infrastructure/telemetry/tracing/span_flush_job.d.ts +25 -0
  77. package/dist/infrastructure/telemetry/tracing/span_flush_job.js +42 -0
  78. package/dist/infrastructure/telemetry/tracing/span_queue_repository.d.ts +82 -0
  79. package/dist/infrastructure/telemetry/tracing/span_queue_repository.js +134 -0
  80. package/dist/infrastructure/telemetry/tracing/span_sampler.d.ts +40 -0
  81. package/dist/infrastructure/telemetry/tracing/span_sampler.js +75 -0
  82. package/dist/infrastructure/telemetry/tracing/span_serializer.d.ts +48 -0
  83. package/dist/infrastructure/telemetry/tracing/span_serializer.js +36 -0
  84. package/dist/infrastructure/telemetry/tracing/sqlite_span_exporter.d.ts +26 -0
  85. package/dist/infrastructure/telemetry/tracing/sqlite_span_exporter.js +47 -0
  86. package/dist/infrastructure/telemetry/tracing/tracer.d.ts +22 -0
  87. package/dist/infrastructure/telemetry/tracing/tracer.js +45 -0
  88. package/dist/infrastructure/telemetry/tracing/tracing_hooks.d.ts +10 -0
  89. package/dist/infrastructure/telemetry/tracing/tracing_hooks.js +59 -0
  90. package/dist/infrastructure/telemetry/tracing_facade.d.ts +50 -0
  91. package/dist/infrastructure/telemetry/tracing_facade.js +139 -0
  92. package/dist/infrastructure/telemetry/types.d.ts +120 -0
  93. package/dist/infrastructure/telemetry/types.js +1 -0
  94. package/dist/ui/components/molecules/TimeZoneSelectorModal.js +7 -1
  95. package/package.json +17 -11
package/README.md CHANGED
@@ -1,91 +1,1107 @@
1
1
  # @sincpro/mobile
2
2
 
3
- Core del **framework móvil Sincpro** (React Native / Expo): infraestructura offline-first (DB, cola/eventos, cron), patrones DDD, adapters base, composición **module-driven** y el `AppShell`. Consume el design system `@sincpro/mobile-ui`.
3
+ **Enterprise mobile application framework for React Native / Expo** — built on hexagonal architecture, domain-driven design, and offline-first infrastructure.
4
4
 
5
- > 🤖 **¿Sos un agente de IA?** Leé primero [`AGENTS.md`](AGENTS.md) — orientación rápida del ecosistema, patrones y trampas. Si algo se comporta raro, [`docs/GOTCHAS.md`](docs/GOTCHAS.md).
5
+ Enterprise mobile development has always been hard. Applications need to work without internet, recover from failures gracefully, sync data when connectivity returns, manage authentication state, handle background tasks, print documents, and do all of this across iOS and Android with a single codebase. Each project that solves these problems from scratch solves them differently — and then someone has to maintain those differences forever.
6
6
 
7
- ## Filosofía
7
+ `@sincpro/mobile` is Sincpro's answer to that problem. This framework encodes proven enterprise patterns once — event queues, dead-letter queues, domain event sourcing, offline-first storage, cron workers, adapter ports — and every app built on it inherits those capabilities by default.
8
8
 
9
- El objetivo es **generar apps móviles de negocio muy rápido** componiendo piezas estables, no escribir cada app desde cero. Principios:
9
+ **Sincpro maintains this.** The source is open and anyone can read it. What Sincpro provides is the commitment behind it: stable contracts, long-term API support, updates as platforms evolve, and people accountable for the behaviors your business depends on. This is not a project that gets abandoned. It is infrastructure for the new era of enterprise mobile development, and Sincpro intends to maintain it for the teams and clients that build on top of it.
10
10
 
11
- - **Framework, no monolito.** Un ecosistema de paquetes publicables; cada app es delgada y declara solo sus dominios.
12
- - **Module-driven + inversión de dependencia.** El core no conoce los negocios. Cada dominio es un `DomainModule` que la app registra; el `orchestrator` alimenta cola, repos, migraciones, cron y temas desde los módulos registrados. Agregar/mover un dominio no toca el core.
13
- - **Offline-first.** SQLite + cola de eventos persistente (con DLQ y reintentos) + sincronización; la app funciona sin red y reconcilia al volver.
14
- - **Hexagonal.** El dominio define puertos (`IPrinterDriver`, `IRepository`, `IRemoteClient`); los adapters concretos viven afuera y se inyectan. El core es agnóstico de hardware y de backend.
15
- - **Frontera real verificable.** Capas con dirección estricta `apps → mobile-odoo → mobile → mobile-ui`; el design system no sabe de negocio; los enums/eventos tienen un punto único de export.
11
+ > **AI agent?** Read [`AGENTS.md`](AGENTS.md) first — ecosystem orientation, patterns, and traps. Known runtime issues live in [`docs/GOTCHAS.md`](docs/GOTCHAS.md).
12
+ >
13
+ > **Deep engineering docs** (component + sequence diagrams, design rationale) live in [`docs/`](docs/README.md): [core architecture](docs/architecture-core.md), [telemetry](docs/architecture-telemetry.md), and the [consumer app structure](docs/architecture-consumer.md). This README is the summary.
16
14
 
17
- ## Ecosistema
15
+ ---
18
16
 
19
- | Paquete | Rol |
20
- | ---------------------- | ------------------------------------------------------------------------------------------- |
21
- | `@sincpro/mobile` | Core: infra, patrones DDD, `framework/` (DomainModule, createApp, orchestrator), `AppShell` |
22
- | `@sincpro/mobile-ui` | Design system standalone (sin router, sin dominios) |
23
- | `@sincpro/mobile-odoo` | Integración Odoo **opcional** (OdooClient, auth, server, partner) |
24
- | `sincpro-mobile-<app>` | Apps de negocio (tickets, distribution) que componen lo anterior |
17
+ ## ⚡ Quick Start
25
18
 
26
- ## Instalación
19
+ Here is the minimum to get a working enterprise app running:
27
20
 
28
21
  ```bash
29
22
  npx expo install @sincpro/mobile @sincpro/mobile-ui
30
23
  ```
31
24
 
32
- `@sincpro/mobile-ui` es peer dependency (lo provee la app). Para integrar Odoo: `@sincpro/mobile-odoo` (opcional).
25
+ ```tsx
26
+ // 1. Define your domain module
27
+ import { DomainModule } from "@sincpro/mobile/framework/domain_module";
28
+ import type { Subscriber } from "@sincpro/mobile/domain/event_sourcing";
29
+ import type { CronWorker } from "@sincpro/mobile/infrastructure/workers";
33
30
 
34
- ## Instanciar una app
31
+ class SalesModule extends DomainModule {
32
+ readonly key = "SALES";
33
+ readonly name = "Sales";
35
34
 
36
- Una app define sus dominios como **clases que extienden `DomainModule`** y las compone con `createAppShell`:
35
+ override subscribers(): Subscriber[] {
36
+ return SalesSubscribers;
37
+ }
38
+ override crons(): CronWorker[] {
39
+ return SalesCronJobs;
40
+ }
41
+ }
37
42
 
38
- ```tsx
43
+ const salesModule = new SalesModule();
44
+
45
+ // 2. Wire the app shell — one call does everything
39
46
  import { createAppShell, createTheme } from "@sincpro/mobile";
47
+
48
+ export default createAppShell({
49
+ theme: createTheme({ accent: "#0EA5E9" }),
50
+ domains: [salesModule],
51
+ ui: { [salesModule.key]: SalesScreen },
52
+ activeDomain: salesModule.key,
53
+ });
54
+ ```
55
+
56
+ `createAppShell` runs database migrations, starts the event queue, registers cron workers, applies the theme, loads fonts, and mounts the app — all from a single configuration object.
57
+
58
+ ---
59
+
60
+ ## 📑 Table of Contents
61
+
62
+ 1. [Why This Framework Exists](#why-this-framework-exists)
63
+ 2. [Architecture Overview](#architecture-overview)
64
+ 3. [Ecosystem](#ecosystem)
65
+ 4. [Key Features](#key-features)
66
+ 5. [Creating a DomainModule](#creating-a-domainmodule)
67
+ 6. [AppShell Configuration](#appshell-configuration)
68
+ 7. [Theme Configuration](#theme-configuration)
69
+ 8. [Brand Font Configuration](#brand-font-configuration)
70
+ 9. [Public API](#public-api)
71
+ 10. [Telemetry Architecture](#telemetry-architecture)
72
+ 11. [Subpaths](#subpaths)
73
+ 12. [Development](#development)
74
+
75
+ ---
76
+
77
+ ## 🔍 Why This Framework Exists
78
+
79
+ ### The Enterprise Mobile Problem
80
+
81
+ Building a business mobile application is not the same as building a consumer app. Business apps need to work in warehouses with no Wi-Fi. They need to print receipts to Bluetooth thermal printers. They need to sync order data when the connection comes back, in the right order, without duplicates. They need to run background jobs that don't drain the battery. They need authentication that safely clears sensitive data on logout. They need dark and light themes that match the brand manual. They need to ship fast and be maintained for years.
82
+
83
+ None of this is hard in isolation. All of it together, across iOS and Android, from scratch, for every project — is expensive, inconsistent, and difficult to maintain.
84
+
85
+ ### The AI Era Problem
86
+
87
+ AI tools can generate code faster than ever. That makes the absence of a framework _more_ dangerous, not less. When there is no shared architecture, AI-generated code is consistent only with itself — each prompt produces a different pattern, a different naming convention, a different approach to the same problem. The velocity is real. The maintenance cost is also real.
88
+
89
+ With `@sincpro/mobile`, AI tools have clear patterns to follow: `DomainModule`, `createAppShell`, `Subscriber`, `CronWorker`, `Entity.addDomainEvent`. Code generated within the framework is framework-compliant by default. Human reviewers can audit it quickly. The codebase stays navigable.
90
+
91
+ ### The Multi-Platform Bet
92
+
93
+ React Native and Expo are the right foundation for enterprise cross-platform mobile in 2025 and beyond. A single TypeScript codebase, native performance, the full JavaScript ecosystem, and Expo's managed platform for OTA updates and native modules. Sincpro commits to maintaining the enterprise layer on top of this foundation as it evolves — consumers get platform upgrades without rewriting their business logic.
94
+
95
+ ### What Sincpro Provides
96
+
97
+ Open-source software is valuable. Maintained open-source software is rare. The difference is people: engineers who triage issues, update dependencies, handle platform breaking changes, document traps they found in production, and preserve behavioral contracts across major versions.
98
+
99
+ Sincpro builds and maintains `@sincpro/mobile` because we build and support the clients that run on it. When a behavior breaks, we fix it. When Expo upgrades their SDK, we absorb the migration. When a new enterprise pattern proves itself in production, we promote it into the framework so every app inherits it. That is the commitment.
100
+
101
+ ---
102
+
103
+ ## 🏗️ Architecture Overview
104
+
105
+ The framework is organized in strict layers with enforced one-way dependency flow:
106
+
107
+ ```
108
+ ┌─────────────────────────────────────────────────────┐
109
+ │ Business Apps │
110
+ │ sincpro-mobile-tickets / distribution / … │
111
+ └────────────────────────┬────────────────────────────┘
112
+ │
113
+ ┌────────────────────────▼────────────────────────────┐
114
+ │ @sincpro/mobile-odoo (optional) │
115
+ │ OdooClient · Auth · Partner sync │
116
+ └────────────────────────┬────────────────────────────┘
117
+ │
118
+ ┌────────────────────────▼────────────────────────────┐
119
+ │ @sincpro/mobile ◄── YOU ARE HERE │
120
+ │ │
121
+ │ framework/ DomainModule · Kernel · Orchestrator│
122
+ │ domain/ Entity · Event Sourcing · Ports │
123
+ │ infrastructure/ EventBus · CronWorker · DLQ │
124
+ │ adapters/ Network · Bluetooth · Geo · WebView│
125
+ │ services/ Printer · DLQ · Events · Network │
126
+ │ entrypoints/ AppShell · DB · Queue · Cron │
127
+ └────────────────────────┬────────────────────────────┘
128
+ │
129
+ ┌────────────────────────▼────────────────────────────┐
130
+ │ @sincpro/mobile-ui │
131
+ │ Design System — no domain knowledge │
132
+ └─────────────────────────────────────────────────────┘
133
+ ```
134
+
135
+ ### Key Architectural Principles
136
+
137
+ - **Hexagonal Architecture (Ports and Adapters).** The domain layer defines interfaces (`IPrinterDriver`, `IRepository`). Concrete implementations live outside and are injected. The core never imports hardware SDKs.
138
+ - **Domain-Driven Design.** Each bounded context is a `DomainModule`. The framework orchestrates them; they never know about each other.
139
+ - **Offline-First.** Every domain event is persisted to SQLite before it is processed. Events survive app restarts and process them on recovery.
140
+ - **Strict layer boundaries.** A lower layer cannot import from a higher one. `mobile-ui` has no imports from `@sincpro/mobile`.
141
+
142
+ ### Base infrastructure — events, queues, repositories, orchestrator
143
+
144
+ `createApp` configures the **Orchestrator**, which bootstraps the **Kernel** (the set of `DomainModule`s). Each module contributes subscribers, crons, repositories and migrations; the orchestrator wires them and starts the workers. Everything durable is a SQLite-backed queue drained by a worker — offline-first by construction.
145
+
146
+ ```mermaid
147
+ flowchart TB
148
+ createApp["createApp(config)"] --> orch["Orchestrator<br/>configure · bootstrap"]
149
+ orch --> kernel["Kernel<br/>aggregates DomainModules"]
150
+
151
+ subgraph MODULES["DomainModule(s) — bounded contexts"]
152
+ mod["module.key / name<br/>subscribers() · crons()<br/>repositories() · migrations()"]
153
+ end
154
+ kernel --> mod
155
+
156
+ subgraph WORKERS["Background workers"]
157
+ qp["EventBus / QueueProcessor<br/>polls + dispatches, connectivity-gated"]
158
+ crons["CronWorker(s)<br/>cron.sync — setInterval &lt;15m / OS task ≥15m"]
159
+ end
160
+
161
+ subgraph STORE["SQLite (migrations on bootstrap)"]
162
+ ev[("event_queue")]
163
+ de[("domain_events")]
164
+ dlq[("dead_letter_queue")]
165
+ ddl[("domain_events_dead_letter")]
166
+ settings[("settings")]
167
+ tq[("telemetry_queue")]
168
+ sq[("spans_queue")]
169
+ end
170
+
171
+ subgraph REPOS["Repositories (adapters/ports)"]
172
+ repo["Entity & queue repositories<br/>DomainEventDeadLetterRepository · …"]
173
+ end
174
+
175
+ netcron["CHECK_NETWORK cron"] -->|emits| netev["InternetIsUp / Down<br/>(UIEventBus)"]
176
+ netev -.->|gates draining| qp
177
+
178
+ orch --> qp
179
+ orch --> crons
180
+ kernel -->|migrations| STORE
181
+ mod -->|subscribers| qp
182
+ mod -->|crons| crons
183
+ mod -->|repositories| repo
184
+
185
+ qp -->|read/ack| de
186
+ qp -->|on failure| ddl
187
+ repo --> de
188
+ repo --> dlq
189
+ entity["Domain Entity<br/>addDomainEvent()"] -->|persist before process| de
190
+ ```
191
+
192
+ ---
193
+
194
+ ## 🗂️ Ecosystem
195
+
196
+ | Package | Role |
197
+ | ---------------------- | -------------------------------------------------------------------- |
198
+ | `@sincpro/mobile` | Core framework: infrastructure, DDD patterns, `AppShell` |
199
+ | `@sincpro/mobile-ui` | Standalone design system (no domain logic) |
200
+ | `@sincpro/mobile-odoo` | **Optional** Odoo 17 integration (OdooClient, auth, partner, server) |
201
+ | `sincpro-mobile-<app>` | Business apps (tickets, distribution) that compose the above |
202
+
203
+ Dependency direction is enforced: apps use everything; `mobile-ui` knows nothing upstream.
204
+
205
+ ---
206
+
207
+ ## 🔑 Key Features
208
+
209
+ ### ✅ Module-Driven Architecture
210
+
211
+ Each bounded context is a class that extends `DomainModule`. It declares its own repositories, migrations, subscribers, and cron workers. The `Kernel` aggregates them all; the `Orchestrator` manages their lifecycle. The core never changes when a domain is added or removed.
212
+
213
+ ```tsx
214
+ class InvoiceModule extends DomainModule {
215
+ readonly key = "INVOICE";
216
+ readonly name = "Invoice";
217
+
218
+ override repositories() {
219
+ return {
220
+ invoiceRepo: new InvoiceRepository(),
221
+ invoiceLineRepo: new InvoiceLineRepository(),
222
+ };
223
+ }
224
+
225
+ override migrations(): IMigration[] {
226
+ return [CreateInvoicesTable, CreateInvoiceLinesTable];
227
+ }
228
+
229
+ override subscribers(): Subscriber[] {
230
+ return [InvoiceCreatedSubscriber, InvoiceVoidedSubscriber, SyncInvoiceSubscriber];
231
+ }
232
+
233
+ override crons(): CronWorker[] {
234
+ return [SyncPendingInvoicesCron];
235
+ }
236
+
237
+ // Tables to preserve when the domain is wiped (e.g. on logout)
238
+ override persistOnReset(): string[] {
239
+ return ["invoice_templates"];
240
+ }
241
+ }
242
+ ```
243
+
244
+ ### ✅ Offline-First Event Queue
245
+
246
+ Every domain event is persisted to SQLite before any subscriber processes it. If the app is killed mid-processing, the event is replayed on restart. Events requiring network connectivity are held until connectivity returns.
247
+
248
+ **Poll cycle:** EventBus polls every **800ms**, with exponential backoff (×1.5 multiplier, max **30s**) when the queue is empty or the device is offline. The interval resets immediately when a new event is published or connectivity is restored.
249
+
250
+ ```tsx
251
+ import { Entity } from "@sincpro/mobile/domain/entity";
252
+
253
+ class Order extends Entity {
254
+ status = "pending";
255
+
256
+ confirm() {
257
+ this.status = "confirmed";
258
+ // Persisted to SQLite before processig — survives app restart
259
+ this.addDomainEvent(OrderConfirmedEvent, { orderId: this.uuid });
260
+ }
261
+ }
262
+
263
+ // Subscriber processes the event asynchronously
264
+ class OrderConfirmedSubscriber extends Subscriber {
265
+ listen = [OrderConfirmedEvent];
266
+ requiresAuth = true;
267
+
268
+ async process(event: OrderConfirmedEvent): Promise<void> {
269
+ await syncOrderToServer(event.orderId);
270
+ }
271
+ }
272
+ ```
273
+
274
+ ### ✅ Dead Letter Queue (DLQ)
275
+
276
+ Events that exceed the maximum retry attempts (`MAX_ATTEMPTS = 1` per cycle) are automatically moved to the dead letter queue with the full error message and failure timestamp. Operations teams can inspect, retry in bulk, or permanently delete failed events.
277
+
278
+ ```tsx
279
+ import { deadLetterQueueUseCases } from "@sincpro/mobile/services/dlq.service";
280
+
281
+ // Inspect failed events
282
+ const failed = await deadLetterQueueUseCases.getFailedEvents();
283
+
284
+ // Retry a single event (fresh uuid, reset attempts)
285
+ await deadLetterQueueUseCases.retryFailedEvent(failed[0]);
286
+
287
+ // Bulk retry — returns { success: number, failed: number }
288
+ const result = await deadLetterQueueUseCases.retryMultipleFailedEvents(failed);
289
+
290
+ // Remove permanently
291
+ await deadLetterQueueUseCases.deleteFailedEvent(failed[0]);
292
+ ```
293
+
294
+ ### ✅ Domain Entity with Event Sourcing
295
+
296
+ `Entity` is the aggregate root. It collects domain events during a business operation and publishes them atomically. Events carry tracing fields (`correlationId`, `aggregateId`, `sequence`) for transaction support and SAGA patterns.
297
+
298
+ ```tsx
299
+ import { Entity } from "@sincpro/mobile/domain/entity";
300
+
301
+ class Invoice extends Entity {
302
+ remoteId?: string;
303
+ status: "draft" | "sent" | "paid" = "draft";
304
+ lines: InvoiceLine[] = [];
305
+
306
+ addLine(line: InvoiceLine) {
307
+ this.lines.push(line);
308
+ // correlationId groups all events from this operation
309
+ this.addDomainEvent(InvoiceLineAddedEvent, { invoiceId: this.uuid, lineId: line.uuid });
310
+ }
311
+
312
+ send() {
313
+ this.status = "sent";
314
+ // All events from this call share the same correlationId and sequence
315
+ this.addDomainEvent(InvoiceSentEvent, { invoiceId: this.uuid, sentAt: new Date() });
316
+ await this.publishAllDomainEvents();
317
+ }
318
+ }
319
+ ```
320
+
321
+ **Entity lifecycle methods:**
322
+
323
+ | Method | Description |
324
+ | --------------------------------- | ----------------------------------------------------------------------------- |
325
+ | `addDomainEvent(Event, payload?)` | Enqueues an event with auto-assigned aggregateId, correlationId, and sequence |
326
+ | `addDomainEventWithEntity(Event)` | Enqueues event with the entire entity as the `record` field |
327
+ | `publishAllDomainEvents()` | Async: persists all pending events, clears the internal queue |
328
+ | `publishAllDomainEventsSync()` | Sync: publishes immediately, throws on any failure |
329
+ | `copyDomainEventsFrom(source)` | Transfers events from another entity (re-aggregating) |
330
+
331
+ ### ✅ Remote Entity Sync
332
+
333
+ `RemoteEntity` extends `Entity` with sync state tracking for entities that mirror data from a remote server. It manages the full lifecycle from pending local creation to confirmed remote sync.
334
+
335
+ ```tsx
336
+ import { RemoteEntity } from "@sincpro/mobile/domain/entity";
337
+
338
+ class Product extends RemoteEntity {
339
+ name: string = "";
340
+ price: number = 0;
341
+ // remoteState: "SYNCED" | "PENDING" | "FAILED"
342
+ // remoteId?: string — the server's ID once synced
343
+
344
+ static fromApiResponse(data: ApiProduct): Product {
345
+ return Product.fromRemoteDTO(data);
346
+ }
347
+
348
+ syncWithServer(updated: ApiProduct) {
349
+ this.mergeWithRemote(updated); // preserves uuid + eventIds
350
+ this.markAsSynced();
351
+ }
352
+
353
+ toApiPayload() {
354
+ return this.remotePayload(); // serializes to API format
355
+ }
356
+ }
357
+ ```
358
+
359
+ ### ✅ Background Cron Workers
360
+
361
+ `CronWorker` schedules recurring background tasks. Workers under 15 minutes use `setInterval` (foreground); workers at or above 15 minutes register with `expo-background-task` for true background execution.
362
+
363
+ ```tsx
364
+ import { CronWorker } from "@sincpro/mobile/infrastructure/workers";
365
+
366
+ const SyncInvoicesCron = new CronWorker(
367
+ "sync-invoices", // task name (must be globally unique)
368
+ async () => {
369
+ const pending = await invoiceRepo.findPendingSync();
370
+ for (const invoice of pending) {
371
+ await apiClient.syncInvoice(invoice);
372
+ }
373
+ },
374
+ 15, // interval in minutes (>= 15 → background capable)
375
+ true, // requiresAuth — won't run if the session is unauthenticated
376
+ );
377
+ ```
378
+
379
+ The `Orchestrator` starts, stops, and re-syncs workers automatically when domains are activated/deactivated or when the authentication state changes.
380
+
381
+ ### ✅ Authentication Lifecycle
382
+
383
+ The `Orchestrator` manages a global authentication state that gates subscribers and cron workers. Marking the session unauthenticated can optionally wipe the event queue and reset the database — leaving only the tables declared in `persistOnReset()`.
384
+
385
+ ```tsx
386
+ import { orchestrator } from "@sincpro/mobile";
387
+
388
+ // User signs in
389
+ await orchestrator.authenticateSession();
390
+ // → Re-syncs queue subscribers and cron workers for active domains
391
+
392
+ // User signs out — clears queue + resets DB (except persisted tables)
393
+ await orchestrator.unauthenticateSession(true);
394
+
395
+ // Resume after app goes to background and returns
396
+ await orchestrator.restoreSession();
397
+ // → Drains the event queue and re-syncs crons
398
+
399
+ // Domain enable / disable at runtime
400
+ await orchestrator.enableDomain("ODOO");
401
+ await orchestrator.disableDomain("ODOO");
402
+ ```
403
+
404
+ All state transitions are protected by a mutex — concurrent calls are serialized.
405
+
406
+ ### ✅ Connectivity Awareness
407
+
408
+ The framework knows whether the device has internet access and routes work accordingly. Events marked `requiresNetwork = true` are held in the queue until connectivity is confirmed. Subscribers can respond directly to connectivity changes via domain events.
409
+
410
+ ```tsx
411
+ class SyncOrderEvent extends DomainEvent {
412
+ static requiresNetwork = true; // Won't process while offline
413
+ }
414
+
415
+ // Check status at any point
416
+ import { networkUseCases } from "@sincpro/mobile/services/network.service";
417
+
418
+ const status = await networkUseCases.getNetworkStatus();
419
+ // { isConnected: boolean, isInternetReachable: boolean | null, type: "wifi"|"cellular"|…, ipAddress }
420
+ ```
421
+
422
+ The EventBus subscribes to `InternetIsDownEvent` / `InternetIsUpEvent` internally — when connectivity is lost it increases the poll interval, and when it is restored it resets to 800ms and drains any held events immediately.
423
+
424
+ ### ✅ Telemetry & Observability (OpenTelemetry + Loki)
425
+
426
+ Distributed tracing and structured logs, **offline-first**, built on the OpenTelemetry SDK. Spans buffer to SQLite and ship to any OTLP/HTTP collector (Grafana Alloy, Tempo, Jaeger); logs ship to Loki. Telemetry is **opt-in** — pass a `telemetry` config to `createApp`.
427
+
428
+ ```tsx
429
+ await createApp({
430
+ telemetry: {
431
+ loki: {
432
+ endpoint: "https://loki.acme.com",
433
+ labels: { app: "pos", env: "prod", tenant: "acme" },
434
+ // Standard auth shortcut…
435
+ auth: { type: "bearer", token: "…" },
436
+ // …or any custom gateway headers (merged after auth, win on conflict)
437
+ headers: { "sincpro-api-key": "…", "X-Scope-OrgID": "acme" },
438
+ },
439
+ otlp: {
440
+ endpoint: "https://alloy.acme.com",
441
+ headers: { Authorization: "Bearer …" },
442
+ },
443
+ flush: {
444
+ backgroundIntervalMin: 15, // 0 disables the background cron
445
+ onConnectivityEvents: true, // drain the backlog the moment the network returns
446
+ sendTimeoutMs: 4000, // fail fast instead of hanging while offline
447
+ },
448
+ },
449
+ });
450
+ ```
451
+
452
+ **One import, simple API.** The `Tracing` facade is the single entry point for custom instrumentation:
453
+
454
+ ```tsx
455
+ import { Tracing } from "@sincpro/mobile/infrastructure/telemetry";
456
+
457
+ // Manual span block — start/end/error handled for you
458
+ await Tracing.withSpan("checkout.process", async (span) => {
459
+ span.setAttribute("order.id", order.id);
460
+ await processOrder(order);
461
+ });
462
+
463
+ // Or decorate — a span per invocation, parent context propagated automatically
464
+ class CheckoutUseCase {
465
+ @Tracing.Trace()
466
+ async process(order: Order) {
467
+ /* … */
468
+ }
469
+ }
470
+
471
+ @Tracing.TraceClass("checkout") // a span per public method + context propagation
472
+ class InventoryService {
473
+ /* … */
474
+ }
475
+
476
+ // Correlate your own logs with the active trace
477
+ logger.info("charge attempt" + Tracing.logSuffix()); // → " trace_id=… span_id=…"
478
+
479
+ // Inspect offline backlog pressure (health checks / dashboards)
480
+ const stats = await Tracing.bufferStats();
481
+ // { logs: { rows, approxBytes, dropped, sampled },
482
+ // spans: { rows, approxBytes, dropped, sampled } }
483
+ // dropped = evicted after buffering (back-pressure);
484
+ // sampled = refused at ingest by head sampling (load shed, spans only)
485
+ ```
486
+
487
+ See [Telemetry Architecture](#telemetry-architecture) for the delivery and retention design and the reasoning behind it.
488
+
489
+ ### ✅ Bluetooth Printer Integration
490
+
491
+ The printer port (`IPrinterDriver`) is defined by the framework; the concrete driver is registered by the app. The `PrinterService` wraps it with connection management, permission handling, and a grace-reconnect strategy.
492
+
493
+ ```tsx
494
+ // Register the adapter before AppShell boots
495
+ import { printerService } from "@sincpro/mobile/services/printer.service";
496
+ import { PrinterAdapter } from "./adapters/Printer.adapter"; // your Bluetooth impl
497
+
498
+ printerService.setDriver(PrinterAdapter);
499
+
500
+ // Then use the service anywhere
501
+ await printerService.connect(deviceAddress, "Printer Name");
502
+ await printerService.printText("=== RECEIPT ===\nItem: Widget\nTotal: $12.00");
503
+ await printerService.printImageBase64(base64Png);
504
+ await printerService.printHtml(htmlString); // converts to PDF (576px thermal width)
505
+
506
+ // Capture a React Native view and print it
507
+ await printerService.printFromView(receiptViewRef);
508
+ ```
509
+
510
+ **Grace reconnect:** If printing fails, the service automatically disconnects, reconnects to the last saved printer, and retries once — without surfacing the error to the user.
511
+
512
+ ### ✅ WebView Print Interception
513
+
514
+ The framework ships injectable scripts that intercept `window.print()` calls inside a WebView, capture the document as a PNG via `html2canvas`, and forward it to React Native for Bluetooth printing.
515
+
516
+ ```tsx
517
+ import { WebViewAdapter } from "@sincpro/mobile/adapters/webview";
518
+
519
+ <WebView
520
+ injectedJavaScriptBeforeContentLoaded={WebViewAdapter.getDefaultInterceptors()
521
+ .map((s) => s.script)
522
+ .join("\n")}
523
+ onMessage={({ nativeEvent }) => {
524
+ // Framework base_module handles PRINT_IMAGE automatically
525
+ // via ProcessWebViewMessageSubscriber → PrintImageSubscriber
526
+ EventBus.publishSync(new WebViewMessageReceivedEvent({ raw: nativeEvent.data }));
527
+ }}
528
+ />;
529
+ ```
530
+
531
+ Available interceptors: `printInterceptor` (default), `ajaxInterceptor` (debug), `contentHeightReporter`.
532
+
533
+ ### ✅ Activity Monitoring
534
+
535
+ `useCommon()` exposes real-time visibility into queue and cron activity — what is running, what failed, and how long it has been running. Use this to build status indicators, loading states, and error recovery UI.
536
+
537
+ ```tsx
538
+ import { useCommon } from "@sincpro/mobile";
539
+
540
+ function StatusBar() {
541
+ const { currentActivity, lastError, isProcessing, dismiss } = useCommon();
542
+
543
+ if (lastError)
544
+ return (
545
+ <Banner variant="error" onDismiss={dismiss}>
546
+ {lastError.label} failed: {lastError.error}
547
+ </Banner>
548
+ );
549
+
550
+ if (isProcessing) return <ActivityIndicator label={currentActivity?.label ?? "Working…"} />;
551
+
552
+ return null;
553
+ }
554
+ ```
555
+
556
+ **`ICommonContext` full shape:**
557
+
558
+ | Field | Type | Description |
559
+ | ------------------------ | ------------------------------- | ---------------------------------------- |
560
+ | `currentActivity` | `Activity \| undefined` | Currently running queue or cron task |
561
+ | `lastError` | `Activity \| undefined` | Last failed task (auto-clears after 10s) |
562
+ | `isProcessing` | `boolean` | True while any task is active or errored |
563
+ | `dismiss()` | `() => void` | Clears `lastError` |
564
+ | `debugMode` | `boolean` | Developer debug toggle (persisted) |
565
+ | `colorScheme` | `"light" \| "dark" \| "system"` | Current color scheme (persisted) |
566
+ | `setColorScheme(scheme)` | function | Switches theme + persists preference |
567
+ | `timezone` | `string \| null` | Persisted timezone setting |
568
+ | `updateTimezone(tz)` | function | Updates and persists |
569
+ | `hasGeoPermission` | `boolean` | Current geo permission state |
570
+ | `requestGeoPermission()` | `async () => boolean` | Requests location permission |
571
+ | `checkGeoPermission()` | `async () => void` | Rechecks without prompting |
572
+
573
+ ### ✅ Domain Switcher
574
+
575
+ Apps that compose multiple domains can switch the active UI at runtime without any navigation library setup. The domain that is not displayed is still running — its queue subscribers and cron workers remain active.
576
+
577
+ ```tsx
578
+ import { useDomainSwitcher } from "@sincpro/mobile";
579
+
580
+ function AppSwitcher() {
581
+ const { activeDomain, switchTo } = useDomainSwitcher();
582
+
583
+ return (
584
+ <Navigation.SegmentedControl
585
+ value={activeDomain}
586
+ options={[
587
+ { value: "TICKETS", label: "Tickets" },
588
+ { value: "ODOO", label: "Odoo" },
589
+ ]}
590
+ onChange={switchTo}
591
+ />
592
+ );
593
+ }
594
+ ```
595
+
596
+ ### ✅ Global Error Handling
597
+
598
+ `DomainException` instances that escape to the React root are bridged to native `Alert` dialogs automatically:
599
+
600
+ ```tsx
601
+ import { installGlobalErrorHandler } from "@sincpro/mobile";
602
+
603
+ // Call once before createAppShell
604
+ installGlobalErrorHandler();
605
+ ```
606
+
607
+ ---
608
+
609
+ ## 🛠️ Creating a DomainModule
610
+
611
+ A `DomainModule` is the primary building block. Here is how to create one from scratch:
612
+
613
+ ### Step 1 — Define the module class
614
+
615
+ ```tsx
40
616
  import { DomainModule } from "@sincpro/mobile/framework/domain_module";
41
617
  import type { Subscriber } from "@sincpro/mobile/domain/event_sourcing";
618
+ import type { CronWorker } from "@sincpro/mobile/infrastructure/workers";
619
+ import type { IMigration } from "@sincpro/mobile/domain/database";
42
620
 
43
- class VentasModule extends DomainModule {
44
- readonly key = "VENTAS";
45
- readonly name = "Ventas";
621
+ export class TicketsModule extends DomainModule {
622
+ readonly key = "TICKETS"; // unique — used as the ui map key
623
+ readonly name = "Tickets"; // human-readable
624
+
625
+ override repositories() {
626
+ return {
627
+ ticketRepo: new TicketRepository(),
628
+ ticketLineRepo: new TicketLineRepository(),
629
+ };
630
+ }
631
+
632
+ override migrations(): IMigration[] {
633
+ return [CreateTicketsTable, CreateTicketLinesTable];
634
+ }
46
635
 
47
636
  override subscribers(): Subscriber[] {
48
- return [];
637
+ return [TicketCreatedSubscriber, TicketPrintSubscriber, SyncTicketSubscriber];
49
638
  }
639
+
640
+ override crons(): CronWorker[] {
641
+ return [SyncPendingTicketsCron];
642
+ }
643
+
644
+ // Preserved during logout wipe — ticket templates survive session reset
645
+ override persistOnReset(): string[] {
646
+ return ["ticket_templates", "ticket_categories"];
647
+ }
648
+ }
649
+
650
+ export const ticketsModule = new TicketsModule();
651
+ ```
652
+
653
+ ### Step 2 — Module hook reference
654
+
655
+ | Hook | Returns | When it runs | Can assume |
656
+ | ------------------ | ----------------------------- | ------------------------- | ------------------ |
657
+ | `repositories()` | `Record<string, IRepository>` | Kernel bootstrap | Nothing yet |
658
+ | `migrations()` | `IMigration[]` | After repos registered | Repos registered |
659
+ | `subscribers()` | `Subscriber[]` | After migrations | DB schema ready |
660
+ | `crons()` | `CronWorker[]` | After subscribers | Queue running |
661
+ | `persistOnReset()` | `string[]` table names | On `unauthenticate(true)` | Called during wipe |
662
+
663
+ All hooks return safe empty defaults. Override only what your domain needs.
664
+
665
+ ### Step 3 — Wire the module in the app shell
666
+
667
+ ```tsx
668
+ export function createTicketsApp(): ComponentType {
669
+ // Register adapters before the shell boots
670
+ printerService.setDriver(PrinterAdapter);
671
+
672
+ return createAppShell({
673
+ theme: createTheme(TICKETS_THEME),
674
+ darkTheme: createTheme(TICKETS_DARK_THEME),
675
+ brandFont: { files: { … }, families: { … } },
676
+ domains: [odooModule, ticketsModule],
677
+ ui: { [ticketsModule.key]: TicketsApp },
678
+ activeDomain: ticketsModule.key,
679
+ providers: [ConfirmationProvider, ProcessToastProvider],
680
+ splashComponent: TicketsSplash,
681
+ });
50
682
  }
683
+ ```
684
+
685
+ ### Recommended project layout
686
+
687
+ ```
688
+ sincpro_mobile_myapp/
689
+ ├── assets/
690
+ │ └── fonts/ ← brand typeface files (.otf / .ttf)
691
+ ├── sincpro_mobile_myapp/
692
+ │ ├── entrypoints/
693
+ │ │ ├── main.tsx ← DomainModule class + createMyApp()
694
+ │ │ ├── cron/ ← CronWorker definitions
695
+ │ │ ├── queue/ ← Subscriber definitions
696
+ │ │ └── ui/
697
+ │ │ ├── App.tsx ← root screen component
698
+ │ │ └── theme/
699
+ │ │ └── tokens.ts ← ThemeTokens (light + dark)
700
+ │ ├── adapters/ ← concrete port implementations (Printer, API, …)
701
+ │ ├── domain/ ← entities, value objects, domain events, port interfaces
702
+ │ ├── application/ ← use cases
703
+ │ └── infrastructure/ ← repositories, remote clients
704
+ └── app/
705
+ └── index.tsx ← calls createMyApp(), exports default
706
+ ```
51
707
 
52
- const ventasModule = new VentasModule();
708
+ ---
709
+
710
+ ## ⚙️ AppShell Configuration
711
+
712
+ `createAppShell(config)` is the single composition root. All infrastructure wires up from this one call.
713
+
714
+ ```tsx
715
+ import { createAppShell, createTheme } from "@sincpro/mobile";
53
716
 
54
717
  export default createAppShell({
55
- theme: createTheme({ primary: "#0EA5E9" }),
56
- domains: [ventasModule],
57
- ui: { [ventasModule.key]: VentasScreen },
58
- activeDomain: ventasModule.key,
718
+ // ── Required ─────────────────────────────────────────────────────────────────
719
+ domains: [odooModule, ticketsModule], // modules to bootstrap (order matters for shared deps)
720
+ ui: { [ticketsModule.key]: TicketsApp }, // domain key → screen component
721
+ activeDomain: ticketsModule.key, // which UI to show on launch
722
+
723
+ // ── Theme ────────────────────────────────────────────────────────────────────
724
+ theme: createTheme(TICKETS_THEME),
725
+ darkTheme: createTheme(TICKETS_DARK_THEME),
726
+
727
+ // ── Brand fonts ──────────────────────────────────────────────────────────────
728
+ brandFont: { files: { … }, families: { … } },
729
+
730
+ // ── Additional React context providers ───────────────────────────────────────
731
+ providers: [ConfirmationProvider, ProcessToastProvider],
732
+
733
+ // ── Splash screen ────────────────────────────────────────────────────────────
734
+ splashComponent: TicketsSplash,
735
+ splashDuration: 2000, // ms minimum (default 2000)
59
736
  });
60
737
  ```
61
738
 
62
- `createAppShell` arranca la infraestructura (migraciones, EventBus, cron, orchestrator), carga fuentes, aplica el tema (a `className` y `style`) y monta el `DomainSwitcher`.
739
+ **Splash gate.** The splash stays visible until **all three** conditions are true simultaneously: domain bootstrap complete, brand fonts loaded by the native font registry, and minimum duration elapsed. This prevents flash-of-unstyled-content and loading flicker on any device speed.
740
+
741
+ **Background domains.** Domains in `domains` without a matching key in `ui` are still fully bootstrapped — their queue, cron, repos, and migrations run normally. They just render no UI. This is how `@sincpro/mobile-odoo` works when included.
742
+
743
+ **Provider wrap order.** `[A, B, C]` renders as `<A><B><C>{app}</C></B></A>`. The last item is the outermost wrapper.
744
+
745
+ ---
746
+
747
+ ## 🎨 Theme Configuration
748
+
749
+ ```tsx
750
+ // entrypoints/ui/theme/tokens.ts
751
+ import type { ThemeTokens } from "@sincpro/mobile-ui/theme/types";
752
+
753
+ export const MY_THEME: ThemeTokens = {
754
+ name: "my-app",
755
+ primary: "#0B0B0B", // buttons, active text
756
+ secondary: "#6B6963", // muted neutral
757
+ accent: "#00C357", // CTA, focus rings, active states
758
+
759
+ bg: {
760
+ page: "#FAFAFA",
761
+ card: "#FFFFFF",
762
+ popover: "#FFFFFF",
763
+ muted: "#EDEBE6",
764
+ accent: "#E6FFF2", // selected surface (tinted)
765
+ hover: "#F0F1EE",
766
+ disabled: "#F1F1EF",
767
+ },
768
+
769
+ text: {
770
+ primary: "#0B0B0B",
771
+ secondary: "#46443F",
772
+ accent: "#006E2E", // text on accent-tinted bg
773
+ inverse: "#FFFFFF",
774
+ onPrimary: "#FFFFFF",
775
+ onAccent: "#FFFFFF",
776
+ // …full shape in @sincpro/mobile-ui README
777
+ },
778
+
779
+ border: { default: "#E4E2DD", light: "#EFEDE8", strong: "#D1D5DB", focus: "#00C357" },
780
+ success: "#00C357", warning: "#FFB020", danger: "#FF4D1F", info: "#201DC4",
781
+ ring: "#00C357",
782
+ input: "#E4E2DD",
783
+ gradient: { primary: ["#1A1A1A", "#0B0B0B"], accent: ["#00A34A", "#00C357"] },
784
+ shadow: { sm: { … }, md: { … }, lg: { … } },
785
+ };
786
+ ```
787
+
788
+ Pass both `theme` and `darkTheme` to `createAppShell`. The framework switches automatically with the system color scheme. `useCommon().colorScheme` exposes the current value and `setColorScheme()` lets the user override it manually (persisted to the settings table).
63
789
 
64
- ## API pública
790
+ ---
65
791
 
66
- - `createAppShell(config)` — composition root de la app (theme, domains, ui, activeDomain, providers).
67
- - `DomainModule` — clase base abstracta de un dominio (`key`, `name`, `shared?`, `repositories()`, `migrations()`, `subscribers()`, `crons()`, `persistOnReset()`).
68
- - `createTheme(overrides?)` — theme tipado (defaults del framework + overrides parciales).
69
- - `createApp(config)` / `Kernel` / `orchestrator` — capa baja de composición (bootstrap sin UI).
70
- - `BaseModule` / `baseModule` — módulo común (COMMON) que el framework registra por defecto.
71
- - `CommonProvider` / `useCommon` — estado común (debug, geo, timezone, actividad de cola/cron).
72
- - `PlainLayout` / `TabNavigatorLayout` — layouts conectados a react-router.
73
- - `installGlobalErrorHandler()` — handler global (errores de dominio → Alert).
792
+ ## 🔤 Brand Font Configuration
74
793
 
75
- ## Subpaths
794
+ The framework loads **only** the fonts you declare — nothing is assumed or loaded silently. Font files are owned by the app; the framework wires them into the DS typography roles.
76
795
 
77
- Las primitivas y capas se importan por subpath, p. ej.:
796
+ ### The four semantic slots (aligned with the brand manual)
797
+
798
+ | Slot | Brand label | Purpose | DS default |
799
+ | --------- | ----------- | -------------------------------- | ---------- |
800
+ | `display` | `df` | Titles, headings, hero text | Inter |
801
+ | `body` | `bf` | Body, labels, buttons (13–15px) | Inter |
802
+ | `caption` | `cf` | Captions, overlines, metadata | Fira Code |
803
+ | `mono` | `mono` | Amounts, SKU codes, numeric data | Fira Code |
804
+
805
+ Most apps only need to declare `display`. Omitting a slot keeps the DS default.
806
+
807
+ ```tsx
808
+ import { FiraCode_400Regular, FiraCode_500Medium } from "@expo-google-fonts/fira-code";
809
+ import {
810
+ Inter_300Light,
811
+ Inter_400Regular,
812
+ Inter_500Medium,
813
+ Inter_600SemiBold,
814
+ Inter_800ExtraBold,
815
+ } from "@expo-google-fonts/inter";
816
+
817
+ createAppShell({
818
+ brandFont: {
819
+ files: {
820
+ // df — Display (brand typeface, local .otf files)
821
+ "Satoshi-Regular": require("../../assets/fonts/Satoshi-Regular.otf"),
822
+ "Satoshi-Medium": require("../../assets/fonts/Satoshi-Medium.otf"),
823
+ "Satoshi-Bold": require("../../assets/fonts/Satoshi-Bold.otf"),
824
+ "Satoshi-Black": require("../../assets/fonts/Satoshi-Black.otf"),
825
+ // bf — Body (Inter via @expo-google-fonts/inter)
826
+ Inter_300Light,
827
+ Inter_400Regular,
828
+ Inter_500Medium,
829
+ Inter_600SemiBold,
830
+ Inter_800ExtraBold,
831
+ // cf + mono (Fira Code via @expo-google-fonts/fira-code)
832
+ FiraCode_400Regular,
833
+ FiraCode_500Medium,
834
+ },
835
+ families: {
836
+ display: {
837
+ regular: "Satoshi-Regular",
838
+ medium: "Satoshi-Medium",
839
+ bold: "Satoshi-Bold",
840
+ black: "Satoshi-Black",
841
+ },
842
+ body: {
843
+ light: "Inter_300Light",
844
+ regular: "Inter_400Regular",
845
+ medium: "Inter_500Medium",
846
+ semiBold: "Inter_600SemiBold",
847
+ extraBold: "Inter_800ExtraBold",
848
+ },
849
+ caption: { regular: "FiraCode_400Regular", medium: "FiraCode_500Medium" },
850
+ mono: { regular: "FiraCode_400Regular", medium: "FiraCode_500Medium" },
851
+ },
852
+ },
853
+ });
854
+ ```
855
+
856
+ ---
857
+
858
+ ## 📖 Public API
859
+
860
+ ### `createAppShell(config)` → `ComponentType`
861
+
862
+ Composition root. Returns a React component ready to export as the Expo entry point.
863
+
864
+ ### `DomainModule` (abstract class)
865
+
866
+ ```tsx
867
+ abstract class DomainModule {
868
+ abstract readonly key: string; // unique, used as ui map key
869
+ abstract readonly name: string; // human-readable label
870
+ readonly shared = false; // shared modules bootstrap first, cannot be disabled
871
+
872
+ repositories(): Record<string, object> {
873
+ return {};
874
+ }
875
+ migrations(): IMigration[] {
876
+ return [];
877
+ }
878
+ subscribers(): Subscriber[] {
879
+ return [];
880
+ }
881
+ crons(): CronWorker[] {
882
+ return [];
883
+ }
884
+ persistOnReset(): string[] {
885
+ return [];
886
+ }
887
+ }
888
+ ```
889
+
890
+ ### `createTheme(overrides?)` → `ThemeTokens`
891
+
892
+ Deep-merges partial overrides on top of the DS base theme.
893
+
894
+ ### `createApp(config?)` → `Promise<Kernel>`
895
+
896
+ Low-level bootstrap without UI. Use for headless contexts (background workers, test harnesses).
897
+
898
+ ### `orchestrator`
899
+
900
+ Singleton managing authentication state, domain activation, and queue/cron sync. Methods: `authenticateSession()`, `unauthenticateSession(cleanData?)`, `restoreSession()`, `enableDomain(key)`, `disableDomain(key)`, `isSessionAuthenticated()`.
901
+
902
+ ### `useCommon()` → `ICommonContext`
903
+
904
+ ```tsx
905
+ const {
906
+ currentActivity,
907
+ lastError,
908
+ isProcessing,
909
+ dismiss,
910
+ colorScheme,
911
+ setColorScheme,
912
+ timezone,
913
+ updateTimezone,
914
+ hasGeoPermission,
915
+ requestGeoPermission,
916
+ debugMode,
917
+ } = useCommon();
918
+ ```
919
+
920
+ ### `useDomainSwitcher()` → `{ activeDomain, switchTo }`
921
+
922
+ ```tsx
923
+ const { activeDomain, switchTo } = useDomainSwitcher();
924
+ switchTo("ODOO");
925
+ ```
926
+
927
+ ### `installGlobalErrorHandler()`
928
+
929
+ Bridges `DomainException` to native `Alert`. Call once before `createAppShell`.
930
+
931
+ ### `baseModule` / `BaseModule`
932
+
933
+ Built-in COMMON module. Always registered automatically — do not pass it to `domains`.
934
+
935
+ ---
936
+
937
+ ## 📡 Telemetry Architecture
938
+
939
+ This section documents how the telemetry pipeline is built and **why**. The design target is an enterprise device fleet that is frequently offline, must never block the UI thread, and must never lose business-critical data to observability.
940
+
941
+ ### Layering
942
+
943
+ Telemetry depends on the context manager one-directionally; nothing in the context manager imports telemetry (no cycles).
944
+
945
+ ```
946
+ @Trace / @TraceClass / withSpan ← instrumentation surface (Tracing facade)
947
+ │ starts spans, sets parent via the OTel Context
948
+ ▼
949
+ ContextManager (stack-based) ← carries the active OTel context across calls
950
+ │ Hermes has no AsyncLocalStorage → an explicit push/pop stack
951
+ ▼
952
+ OpenTelemetry SDK (BasicTracerProvider)
953
+ │ SimpleSpanProcessor → SQLiteSpanExporter
954
+ ▼
955
+ SQLite buffers (spans_queue, telemetry_queue)
956
+ │ drained by the single FlushTelemetry use case
957
+ ▼
958
+ OTLP/HTTP collector + Loki
959
+ ```
960
+
961
+ **Why a stack-based context manager.** Hermes (React Native's engine) has no `AsyncLocalStorage`, so the framework carries the active OpenTelemetry context in an explicit push/pop stack (`StackContextManager`). Spans created inside a `withSpan`/`@Trace` body see the enclosing span as their parent because the parent context sits on top of the stack. The trade-off — concurrent `Promise.all` branches share the top of stack — is documented and acceptable for the sequential flows mobile apps actually run.
962
+
963
+ **Why we wrap OpenTelemetry instead of reinventing it.** Spans, context propagation and OTLP serialization are standards with sharp edges; the framework uses the official OTel SDK for all of it. The only custom piece is the `SQLiteSpanExporter` (an OTel `SpanExporter` that writes to SQLite instead of the network) plus a thin DX layer — the `Tracing` / `ContextManager` facades — so application developers get one import and a small, hard-to-misuse API.
964
+
965
+ ### Component diagram
966
+
967
+ ```mermaid
968
+ flowchart TB
969
+ app["App code<br/>@Trace · @TraceClass · withSpan · logger"]
970
+
971
+ subgraph FACADE["Public facades (one import)"]
972
+ tracing["Tracing<br/>init · withSpan · Trace · TraceClass<br/>activeSpan · logSuffix · bufferStats"]
973
+ ctx["ContextManager<br/>StackContextManager · OTEL_CTX_KEY"]
974
+ end
975
+
976
+ subgraph SDK["OpenTelemetry SDK (Hermes-safe)"]
977
+ provider["BasicTracerProvider"]
978
+ proc["SimpleSpanProcessor"]
979
+ exporter["SQLiteSpanExporter<br/>(custom — writes to SQLite)"]
980
+ end
981
+
982
+ subgraph BUFFERS["Offline buffers (SQLite)"]
983
+ spanRepo["SpanQueueRepository<br/>+ SpanSampler (head sampling)<br/>trace-coherent eviction"]
984
+ logRepo["LogQueueRepository<br/>severity / byte eviction"]
985
+ spansTbl[("spans_queue")]
986
+ logsTbl[("telemetry_queue")]
987
+ end
988
+
989
+ subgraph DELIVERY["Delivery — one use case, three triggers"]
990
+ signal["TelemetrySignal<br/>(debounced, in-memory)"]
991
+ worker["TelemetryFlushWorker<br/>signal · InternetIsUp · bg cron"]
992
+ flush["FlushTelemetry.run()"]
993
+ conn["ConnectivityState"]
994
+ loki["LokiClient<br/>push (opportunistic) · deliver (batch)"]
995
+ otlp["OtlpClient"]
996
+ end
997
+
998
+ collector[["OTLP/HTTP collector<br/>(Alloy · Tempo · Jaeger)"]]
999
+ lokiSrv[["Loki"]]
1000
+
1001
+ app --> tracing
1002
+ app -->|logger.push| loki
1003
+ tracing --> provider
1004
+ tracing -.->|active context| ctx
1005
+ provider --> proc
1006
+ proc --> exporter
1007
+ exporter --> spanRepo
1008
+ spanRepo --> spansTbl
1009
+ exporter -->|onEnqueued / store-first| signal
1010
+ loki -->|on failure| logRepo
1011
+ logRepo --> logsTbl
1012
+
1013
+ signal --> flush
1014
+ worker --> flush
1015
+ flush -->|drain logs| logRepo
1016
+ flush -->|drain spans| spanRepo
1017
+ flush --> loki
1018
+ loki -->|batch| lokiSrv
1019
+ flush --> otlp
1020
+ otlp --> collector
1021
+ flush -.->|updates online flag| conn
1022
+ spanRepo -.->|bufferStats| tracing
1023
+ logRepo -.->|bufferStats| tracing
1024
+ ```
1025
+
1026
+ ### Delivery — event-driven, not a fixed timer
1027
+
1028
+ Telemetry is delivered by a single `FlushTelemetry` use case that drains **both** queues. Three independent triggers funnel into it; **none is required**, so the framework never depends on the host app's connectivity cron or its event bus:
1029
+
1030
+ 1. **Production signal (always on, primary).** Producing telemetry schedules a _debounced_ in-memory flush (`TelemetrySignal`, ~3 s). A burst of spans collapses into one delivery attempt. Spans are persisted to SQLite first, _then_ the signal fires — **store-first**, so a crash mid-send never loses data and batches stay efficient.
1031
+ 2. **Reconnect events (opt-in).** If the app emits `InternetIsUp/Down`, reconnecting drains the backlog immediately.
1032
+ 3. **Background cron (opt-in).** A long-interval (≥15 min → real OS background task) safety net for whatever the other two missed.
1033
+
1034
+ **Why not a 1-minute timer.** A fixed timer fires whether or not there is anything to send and whether or not the device is online, wasting work and hammering a dead network. Offline recovery instead rides the jobs' own **exponential backoff**: a failed delivery backs off and retries, so the pipeline self-heals when the network returns even with no cron and no events. A hard `fetch` timeout (`AbortController`) prevents a flush from hanging while offline — that is the "is there internet?" check, without coupling to a connectivity service.
1035
+
1036
+ ### Retention — bounded, coherent, severity-aware
1037
+
1038
+ Telemetry is **best-effort and bounded by nature** — a device offline for days cannot buffer forever. The framework's job is to lose data _intelligently and visibly_, never silently. Business-critical data does **not** belong here; it goes through the durable `event_queue` / `domain_events` path with its dead-letter queue.
1039
+
1040
+ Both buffers are capped by **row count and payload bytes** (predictable storage regardless of attribute size). When a cap is exceeded:
1041
+
1042
+ - **Spans evict whole traces.** The oldest _entire trace_ is dropped, never individual spans. FIFO-by-row would evict root/parent spans first and leave orphaned children that render as broken traces downstream — the exact failure that makes a trace backend untrustworthy. Survivors are always complete.
1043
+ - **Logs evict by severity (count pressure) or size (byte pressure).** When there are too many rows, the lowest-severity oldest-first go first (`debug → info → warn → error`), so `error`/`warn` survive a flood of `debug`/`info`. When the byte budget is the constraint, the **largest** payloads go first — a single oversized blob is the cause of byte pressure and is shed directly, keeping the maximum number of small, information-dense entries (so keep large blobs out of logs).
1044
+ - **Spans head-sample under pressure.** Once the buffer passes 80 % of either budget, whole _new_ traces are dropped at ingest (a coherent per-trace decision via a deterministic hash, cached so a mid-trace pressure change can't split a trace). This stops the buffer thrashing insert→evict and keeps a coherent _sample_ instead of churning fragments.
1045
+ - **The loss is observable.** `Tracing.bufferStats()` exposes `rows`, `approxBytes`, `dropped` (evicted after buffering — back-pressure) and `sampled` (refused at ingest — load shed) so a rising backlog is a dashboard signal, not a silent gap.
1046
+
1047
+ The eviction scan is **amortized** (runs once every N enqueues), so the hot path stays cheap under offline bursts; the bound is therefore soft (may transiently exceed by up to N rows).
1048
+
1049
+ ---
1050
+
1051
+ ## 📦 Subpaths
78
1052
 
79
1053
  ```ts
1054
+ // Framework primitives
80
1055
  import { DomainModule } from "@sincpro/mobile/framework/domain_module";
1056
+ import { orchestrator } from "@sincpro/mobile/framework/orchestrator";
1057
+ import { Kernel } from "@sincpro/mobile/framework/kernel";
1058
+
1059
+ // Domain layer
81
1060
  import type { Subscriber } from "@sincpro/mobile/domain/event_sourcing";
82
- import { setPrinterDriver } from "@sincpro/mobile/domain/print";
1061
+ import { Entity } from "@sincpro/mobile/domain/entity";
1062
+ import { RemoteEntity } from "@sincpro/mobile/domain/entity";
1063
+ import type { IMigration } from "@sincpro/mobile/domain/database";
1064
+
1065
+ // Infrastructure
1066
+ import type { CronWorker } from "@sincpro/mobile/infrastructure/workers";
1067
+ import { EventBus } from "@sincpro/mobile/infrastructure/workers";
1068
+
1069
+ // Services
1070
+ import { printerService } from "@sincpro/mobile/services/printer.service";
1071
+ import { deadLetterQueueUseCases } from "@sincpro/mobile/services/dlq.service";
1072
+ import { networkUseCases } from "@sincpro/mobile/services/network.service";
1073
+
1074
+ // Adapters
1075
+ import { WebViewAdapter } from "@sincpro/mobile/adapters/webview";
1076
+ import { NetworkAdapter } from "@sincpro/mobile/adapters/network";
1077
+ import { GeoAdapter } from "@sincpro/mobile/adapters/geo";
1078
+ import { ReceiptExporterAdapter } from "@sincpro/mobile/adapters/receipt_exporter";
1079
+
1080
+ // UI components (screens, events)
1081
+ import { PlainLayout } from "@sincpro/mobile/ui/screens/events";
83
1082
  ```
84
1083
 
85
- ## Impresora (opcional)
1084
+ Full map in `package.json` under `exports`.
1085
+
1086
+ ---
86
1087
 
87
- El core es agnóstico del hardware: define el puerto `IPrinterDriver` (`@sincpro/mobile/domain/print`). La app registra un driver concreto al bootstrap (vía `printerService.setDriver(...)`); si no registra ninguno, el driver Noop degrada con un warning.
1088
+ ## 🔧 Development
88
1089
 
89
- ## Desarrollo
1090
+ All tasks run through `make`:
1091
+
1092
+ | Command | What it does |
1093
+ | -------------------- | ------------------------------------------------------ |
1094
+ | `make build` | `tsc` + `tsc-alias` → `dist/` (JS + type declarations) |
1095
+ | `make format` | Auto-fix: ESLint + Prettier + type-check |
1096
+ | `make verify-format` | CI gate — runs `format`, fails if any file changed |
1097
+ | `make publish` | Bump version + `npm publish` |
1098
+
1099
+ **Local monorepo propagation** (no symlinks setup):
1100
+
1101
+ ```bash
1102
+ # After make build — copy dist to each consumer
1103
+ cp -r dist/* ../sincpro_mobile_tickets/node_modules/@sincpro/mobile/dist/
1104
+ cp -r dist/* ../sincpro_mobile_distribution/node_modules/@sincpro/mobile/dist/
1105
+ ```
90
1106
 
91
- Todo vía Makefile. Dos comandos de calidad: `make format` (auto-fix: eslint --fix + prettier + typecheck) y `make verify-format` (gate de CI: corre `format` y falla si quedó algo sin formatear/commitear — cubre lint + formato + tipos). Además `make build` (tsc + tsc-alias → `dist/`) y `make publish`. Detalle del workflow y guardrails en [`AGENTS.md`](AGENTS.md).
1107
+ Detailed workflow, naming conventions, architectural decisions, and AI guardrails: [`AGENTS.md`](AGENTS.md).