@sincpro/mobile 0.3.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +234 -2
- package/dist/entrypoints/cron/EBuiltinCron.d.ts +4 -0
- package/dist/entrypoints/cron/EBuiltinCron.js +5 -0
- package/dist/entrypoints/cron/checkNetworkStatus.cron.js +2 -1
- package/dist/entrypoints/db/migrations.d.ts +3 -1
- package/dist/entrypoints/db/migrations.js +47 -0
- package/dist/entrypoints/queue/QueueProcessor.js +1 -25
- package/dist/entrypoints/queue/subscriber_filter.d.ts +9 -0
- package/dist/entrypoints/queue/subscriber_filter.js +25 -0
- package/dist/entrypoints/ui/common_provider.js +2 -2
- package/dist/framework/base_module.d.ts +1 -0
- package/dist/framework/base_module.js +6 -1
- package/dist/framework/createApp.d.ts +14 -0
- package/dist/framework/createApp.js +6 -0
- package/dist/framework/kernel.d.ts +2 -1
- package/dist/framework/kernel.js +8 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -0
- package/dist/infrastructure/context_manager/context.d.ts +16 -0
- package/dist/infrastructure/context_manager/context.js +37 -0
- package/dist/infrastructure/context_manager/context_api.d.ts +28 -0
- package/dist/infrastructure/context_manager/context_api.js +55 -0
- package/dist/infrastructure/context_manager/context_key.d.ts +15 -0
- package/dist/infrastructure/context_manager/context_key.js +3 -0
- package/dist/infrastructure/context_manager/context_manager_facade.d.ts +107 -0
- package/dist/infrastructure/context_manager/context_manager_facade.js +113 -0
- package/dist/infrastructure/context_manager/decorators/propagate_context.d.ts +24 -0
- package/dist/infrastructure/context_manager/decorators/propagate_context.js +52 -0
- package/dist/infrastructure/context_manager/decorators/with_context.d.ts +34 -0
- package/dist/infrastructure/context_manager/decorators/with_context.js +44 -0
- package/dist/infrastructure/context_manager/index.d.ts +11 -0
- package/dist/infrastructure/context_manager/index.js +12 -0
- package/dist/infrastructure/context_manager/managed_context.d.ts +87 -0
- package/dist/infrastructure/context_manager/managed_context.js +144 -0
- package/dist/infrastructure/context_manager/stack_context_manager.d.ts +39 -0
- package/dist/infrastructure/context_manager/stack_context_manager.js +56 -0
- package/dist/infrastructure/database/connector.js +10 -1
- package/dist/infrastructure/interceptor/index.d.ts +4 -0
- package/dist/infrastructure/interceptor/index.js +2 -0
- package/dist/infrastructure/interceptor/intercept.d.ts +57 -0
- package/dist/infrastructure/interceptor/intercept.js +112 -0
- package/dist/infrastructure/interceptor/logging_hooks.d.ts +22 -0
- package/dist/infrastructure/interceptor/logging_hooks.js +27 -0
- package/dist/infrastructure/logger.d.ts +11 -16
- package/dist/infrastructure/logger.js +61 -18
- package/dist/infrastructure/telemetry/buffer_registry.d.ts +15 -0
- package/dist/infrastructure/telemetry/buffer_registry.js +26 -0
- package/dist/infrastructure/telemetry/connectivity_state.d.ts +20 -0
- package/dist/infrastructure/telemetry/connectivity_state.js +28 -0
- package/dist/infrastructure/telemetry/context_keys.d.ts +7 -0
- package/dist/infrastructure/telemetry/context_keys.js +7 -0
- package/dist/infrastructure/telemetry/fetch_with_timeout.d.ts +8 -0
- package/dist/infrastructure/telemetry/fetch_with_timeout.js +17 -0
- package/dist/infrastructure/telemetry/flush_telemetry.d.ts +40 -0
- package/dist/infrastructure/telemetry/flush_telemetry.js +72 -0
- package/dist/infrastructure/telemetry/index.d.ts +17 -0
- package/dist/infrastructure/telemetry/index.js +17 -0
- package/dist/infrastructure/telemetry/init_telemetry.d.ts +21 -0
- package/dist/infrastructure/telemetry/init_telemetry.js +85 -0
- package/dist/infrastructure/telemetry/logging/flush_job.d.ts +44 -0
- package/dist/infrastructure/telemetry/logging/flush_job.js +69 -0
- package/dist/infrastructure/telemetry/logging/log_queue_repository.d.ts +76 -0
- package/dist/infrastructure/telemetry/logging/log_queue_repository.js +147 -0
- package/dist/infrastructure/telemetry/logging/loki_client.d.ts +32 -0
- package/dist/infrastructure/telemetry/logging/loki_client.js +118 -0
- package/dist/infrastructure/telemetry/logging/loki_registry.d.ts +12 -0
- package/dist/infrastructure/telemetry/logging/loki_registry.js +17 -0
- package/dist/infrastructure/telemetry/telemetry_flush_worker.d.ts +35 -0
- package/dist/infrastructure/telemetry/telemetry_flush_worker.js +66 -0
- package/dist/infrastructure/telemetry/telemetry_signal.d.ts +18 -0
- package/dist/infrastructure/telemetry/telemetry_signal.js +33 -0
- package/dist/infrastructure/telemetry/tracing/active_span.d.ts +22 -0
- package/dist/infrastructure/telemetry/tracing/active_span.js +40 -0
- package/dist/infrastructure/telemetry/tracing/otlp_client.d.ts +13 -0
- package/dist/infrastructure/telemetry/tracing/otlp_client.js +34 -0
- package/dist/infrastructure/telemetry/tracing/otlp_serializer.d.ts +67 -0
- package/dist/infrastructure/telemetry/tracing/otlp_serializer.js +101 -0
- package/dist/infrastructure/telemetry/tracing/span_flush_job.d.ts +25 -0
- package/dist/infrastructure/telemetry/tracing/span_flush_job.js +42 -0
- package/dist/infrastructure/telemetry/tracing/span_queue_repository.d.ts +82 -0
- package/dist/infrastructure/telemetry/tracing/span_queue_repository.js +134 -0
- package/dist/infrastructure/telemetry/tracing/span_sampler.d.ts +40 -0
- package/dist/infrastructure/telemetry/tracing/span_sampler.js +75 -0
- package/dist/infrastructure/telemetry/tracing/span_serializer.d.ts +48 -0
- package/dist/infrastructure/telemetry/tracing/span_serializer.js +36 -0
- package/dist/infrastructure/telemetry/tracing/sqlite_span_exporter.d.ts +26 -0
- package/dist/infrastructure/telemetry/tracing/sqlite_span_exporter.js +47 -0
- package/dist/infrastructure/telemetry/tracing/tracer.d.ts +22 -0
- package/dist/infrastructure/telemetry/tracing/tracer.js +45 -0
- package/dist/infrastructure/telemetry/tracing/tracing_hooks.d.ts +10 -0
- package/dist/infrastructure/telemetry/tracing/tracing_hooks.js +59 -0
- package/dist/infrastructure/telemetry/tracing_facade.d.ts +50 -0
- package/dist/infrastructure/telemetry/tracing_facade.js +139 -0
- package/dist/infrastructure/telemetry/types.d.ts +120 -0
- package/dist/infrastructure/telemetry/types.js +1 -0
- package/dist/infrastructure/workers/CronWorker.d.ts +10 -2
- package/dist/infrastructure/workers/CronWorker.js +18 -5
- package/dist/infrastructure/workers/index.d.ts +1 -1
- package/dist/ui/components/molecules/TimeZoneSelectorModal.js +7 -1
- package/package.json +17 -11
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@ Enterprise mobile development has always been hard. Applications need to work wi
|
|
|
9
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
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.
|
|
12
14
|
|
|
13
15
|
---
|
|
14
16
|
|
|
@@ -66,8 +68,9 @@ export default createAppShell({
|
|
|
66
68
|
7. [Theme Configuration](#theme-configuration)
|
|
67
69
|
8. [Brand Font Configuration](#brand-font-configuration)
|
|
68
70
|
9. [Public API](#public-api)
|
|
69
|
-
10. [
|
|
70
|
-
11. [
|
|
71
|
+
10. [Telemetry Architecture](#telemetry-architecture)
|
|
72
|
+
11. [Subpaths](#subpaths)
|
|
73
|
+
12. [Development](#development)
|
|
71
74
|
|
|
72
75
|
---
|
|
73
76
|
|
|
@@ -136,6 +139,56 @@ The framework is organized in strict layers with enforced one-way dependency flo
|
|
|
136
139
|
- **Offline-First.** Every domain event is persisted to SQLite before it is processed. Events survive app restarts and process them on recovery.
|
|
137
140
|
- **Strict layer boundaries.** A lower layer cannot import from a higher one. `mobile-ui` has no imports from `@sincpro/mobile`.
|
|
138
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 <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
|
+
|
|
139
192
|
---
|
|
140
193
|
|
|
141
194
|
## 🗂️ Ecosystem
|
|
@@ -368,6 +421,71 @@ const status = await networkUseCases.getNetworkStatus();
|
|
|
368
421
|
|
|
369
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.
|
|
370
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
|
+
|
|
371
489
|
### ✅ Bluetooth Printer Integration
|
|
372
490
|
|
|
373
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.
|
|
@@ -816,6 +934,120 @@ Built-in COMMON module. Always registered automatically — do not pass it to `d
|
|
|
816
934
|
|
|
817
935
|
---
|
|
818
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
|
+
|
|
819
1051
|
## 📦 Subpaths
|
|
820
1052
|
|
|
821
1053
|
```ts
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EBuiltinCron } from "../../entrypoints/cron/EBuiltinCron";
|
|
1
2
|
import { loggerCronJobs } from "../../infrastructure/logger";
|
|
2
3
|
import { CronWorker } from "../../infrastructure/workers";
|
|
3
4
|
import { networkUseCases } from "../../services/network.service";
|
|
@@ -6,5 +7,5 @@ async function checkNetworkStatus() {
|
|
|
6
7
|
await networkUseCases.getNetworkStatus();
|
|
7
8
|
loggerCronJobs.info("Finished checking network status");
|
|
8
9
|
}
|
|
9
|
-
const cronCheckNetworkStatus = new CronWorker(
|
|
10
|
+
const cronCheckNetworkStatus = new CronWorker(EBuiltinCron.CHECK_NETWORK, checkNetworkStatus, 2.5);
|
|
10
11
|
export default cronCheckNetworkStatus;
|
|
@@ -4,7 +4,9 @@ export declare enum DATABASE_TABLES {
|
|
|
4
4
|
EVENT_QUEUE = "event_queue",
|
|
5
5
|
DEAD_LETTER_QUEUE = "dead_letter_queue",
|
|
6
6
|
DOMAIN_EVENTS = "domain_events",
|
|
7
|
-
DOMAIN_EVENTS_DEAD_LETTER = "domain_events_dead_letter"
|
|
7
|
+
DOMAIN_EVENTS_DEAD_LETTER = "domain_events_dead_letter",
|
|
8
|
+
TELEMETRY_QUEUE = "telemetry_queue",
|
|
9
|
+
SPANS_QUEUE = "spans_queue"
|
|
8
10
|
}
|
|
9
11
|
declare const MIGRATIONS: IMigration[];
|
|
10
12
|
export default MIGRATIONS;
|
|
@@ -6,6 +6,8 @@ export var DATABASE_TABLES;
|
|
|
6
6
|
DATABASE_TABLES["DEAD_LETTER_QUEUE"] = "dead_letter_queue";
|
|
7
7
|
DATABASE_TABLES["DOMAIN_EVENTS"] = "domain_events";
|
|
8
8
|
DATABASE_TABLES["DOMAIN_EVENTS_DEAD_LETTER"] = "domain_events_dead_letter";
|
|
9
|
+
DATABASE_TABLES["TELEMETRY_QUEUE"] = "telemetry_queue";
|
|
10
|
+
DATABASE_TABLES["SPANS_QUEUE"] = "spans_queue";
|
|
9
11
|
})(DATABASE_TABLES || (DATABASE_TABLES = {}));
|
|
10
12
|
async function createSettingsTable() {
|
|
11
13
|
await DBCursor.execAsync(`
|
|
@@ -110,6 +112,49 @@ async function createDomainEventsDeadLetterTable() {
|
|
|
110
112
|
CREATE INDEX IF NOT EXISTS idx_dead_letter_failed ON ${DATABASE_TABLES.DOMAIN_EVENTS_DEAD_LETTER}(failed_at);
|
|
111
113
|
`);
|
|
112
114
|
}
|
|
115
|
+
async function createTelemetryQueueTable() {
|
|
116
|
+
await DBCursor.execAsync(`
|
|
117
|
+
CREATE TABLE IF NOT EXISTS ${DATABASE_TABLES.TELEMETRY_QUEUE} (
|
|
118
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
119
|
+
level TEXT NOT NULL,
|
|
120
|
+
message TEXT NOT NULL,
|
|
121
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
122
|
+
);
|
|
123
|
+
`);
|
|
124
|
+
await DBCursor.execAsync(`
|
|
125
|
+
CREATE INDEX IF NOT EXISTS idx_telemetry_queue_created
|
|
126
|
+
ON ${DATABASE_TABLES.TELEMETRY_QUEUE}(created_at);
|
|
127
|
+
`);
|
|
128
|
+
}
|
|
129
|
+
async function createSpansQueueTable() {
|
|
130
|
+
await DBCursor.execAsync(`
|
|
131
|
+
CREATE TABLE IF NOT EXISTS ${DATABASE_TABLES.SPANS_QUEUE} (
|
|
132
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
133
|
+
trace_id TEXT NOT NULL,
|
|
134
|
+
span_id TEXT NOT NULL,
|
|
135
|
+
parent_span_id TEXT,
|
|
136
|
+
name TEXT NOT NULL,
|
|
137
|
+
kind INTEGER NOT NULL DEFAULT 0,
|
|
138
|
+
start_time_unixnano TEXT NOT NULL,
|
|
139
|
+
end_time_unixnano TEXT NOT NULL,
|
|
140
|
+
attributes TEXT NOT NULL DEFAULT '{}',
|
|
141
|
+
status_code INTEGER NOT NULL DEFAULT 0,
|
|
142
|
+
status_message TEXT NOT NULL DEFAULT '',
|
|
143
|
+
resource_attrs TEXT NOT NULL DEFAULT '{}',
|
|
144
|
+
events TEXT NOT NULL DEFAULT '[]',
|
|
145
|
+
links TEXT NOT NULL DEFAULT '[]',
|
|
146
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
147
|
+
);
|
|
148
|
+
`);
|
|
149
|
+
await DBCursor.execAsync(`
|
|
150
|
+
CREATE INDEX IF NOT EXISTS idx_spans_queue_created
|
|
151
|
+
ON ${DATABASE_TABLES.SPANS_QUEUE}(created_at);
|
|
152
|
+
`);
|
|
153
|
+
await DBCursor.execAsync(`
|
|
154
|
+
CREATE INDEX IF NOT EXISTS idx_spans_queue_trace
|
|
155
|
+
ON ${DATABASE_TABLES.SPANS_QUEUE}(trace_id);
|
|
156
|
+
`);
|
|
157
|
+
}
|
|
113
158
|
const MIGRATIONS = [
|
|
114
159
|
{ name: DATABASE_TABLES.SETTINGS, migrationFn: createSettingsTable },
|
|
115
160
|
{ name: DATABASE_TABLES.EVENT_QUEUE, migrationFn: createEventQueueTable },
|
|
@@ -119,5 +164,7 @@ const MIGRATIONS = [
|
|
|
119
164
|
name: DATABASE_TABLES.DOMAIN_EVENTS_DEAD_LETTER,
|
|
120
165
|
migrationFn: createDomainEventsDeadLetterTable,
|
|
121
166
|
},
|
|
167
|
+
{ name: DATABASE_TABLES.TELEMETRY_QUEUE, migrationFn: createTelemetryQueueTable },
|
|
168
|
+
{ name: DATABASE_TABLES.SPANS_QUEUE, migrationFn: createSpansQueueTable },
|
|
122
169
|
];
|
|
123
170
|
export default MIGRATIONS;
|
|
@@ -1,33 +1,9 @@
|
|
|
1
1
|
import { loggerQueueProcessor } from "../../infrastructure/logger";
|
|
2
2
|
import { EventBus } from "../../infrastructure/workers";
|
|
3
|
+
import { calculateDesiredSubscribers, statesAreEqual, } from "./subscriber_filter";
|
|
3
4
|
let isRunning = false;
|
|
4
5
|
let lastSyncState = null;
|
|
5
6
|
let currentSubscribers = new Set();
|
|
6
|
-
function statesAreEqual(a, b) {
|
|
7
|
-
if (!a)
|
|
8
|
-
return false;
|
|
9
|
-
if (a.isAuthenticated !== b.isAuthenticated)
|
|
10
|
-
return false;
|
|
11
|
-
if (a.domains.size !== b.domains.size)
|
|
12
|
-
return false;
|
|
13
|
-
for (const domain of a.domains) {
|
|
14
|
-
if (!b.domains.has(domain))
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
return true;
|
|
18
|
-
}
|
|
19
|
-
function calculateDesiredSubscribers(domains, isAuthenticated, subscribersByKey) {
|
|
20
|
-
const desired = new Set();
|
|
21
|
-
for (const domain of domains) {
|
|
22
|
-
const domainSubscribers = subscribersByKey[domain] ?? [];
|
|
23
|
-
for (const subscriber of domainSubscribers) {
|
|
24
|
-
if (isAuthenticated || !subscriber.requiresAuth) {
|
|
25
|
-
desired.add(subscriber);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return desired;
|
|
30
|
-
}
|
|
31
7
|
export const QueueProcessor = {
|
|
32
8
|
start: () => {
|
|
33
9
|
if (isRunning)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface HasRequiresAuth {
|
|
2
|
+
readonly requiresAuth: boolean;
|
|
3
|
+
}
|
|
4
|
+
export interface SyncState {
|
|
5
|
+
domains: Set<string>;
|
|
6
|
+
isAuthenticated: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function statesAreEqual(a: SyncState | null, b: SyncState): boolean;
|
|
9
|
+
export declare function calculateDesiredSubscribers<T extends HasRequiresAuth>(domains: Set<string>, isAuthenticated: boolean, subscribersByKey: Record<string, T[]>): Set<T>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function statesAreEqual(a, b) {
|
|
2
|
+
if (!a)
|
|
3
|
+
return false;
|
|
4
|
+
if (a.isAuthenticated !== b.isAuthenticated)
|
|
5
|
+
return false;
|
|
6
|
+
if (a.domains.size !== b.domains.size)
|
|
7
|
+
return false;
|
|
8
|
+
for (const domain of a.domains) {
|
|
9
|
+
if (!b.domains.has(domain))
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
export function calculateDesiredSubscribers(domains, isAuthenticated, subscribersByKey) {
|
|
15
|
+
const desired = new Set();
|
|
16
|
+
for (const domain of domains) {
|
|
17
|
+
const domainSubscribers = subscribersByKey[domain] ?? [];
|
|
18
|
+
for (const subscriber of domainSubscribers) {
|
|
19
|
+
if (isAuthenticated || !subscriber.requiresAuth) {
|
|
20
|
+
desired.add(subscriber);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return desired;
|
|
25
|
+
}
|
|
@@ -191,12 +191,12 @@ export function CommonProvider({ children, lightTheme, darkTheme }) {
|
|
|
191
191
|
function handleCronStart(payload) {
|
|
192
192
|
if (!payload || typeof payload !== "object" || !("task" in payload))
|
|
193
193
|
return;
|
|
194
|
-
const { task } = payload;
|
|
194
|
+
const { task, label } = payload;
|
|
195
195
|
setCronTasks((prev) => {
|
|
196
196
|
const next = new Map(prev);
|
|
197
197
|
next.set(task, {
|
|
198
198
|
id: task,
|
|
199
|
-
label: task,
|
|
199
|
+
label: label ?? task,
|
|
200
200
|
type: "cron",
|
|
201
201
|
status: "running",
|
|
202
202
|
startedAt: Date.now(),
|
|
@@ -4,7 +4,7 @@ import { DomainEventDeadLetterRepository } from "../adapters/repositories/domain
|
|
|
4
4
|
import { SettingsRepository } from "../adapters/repositories/setting.repository";
|
|
5
5
|
import { ECommonRepository } from "../domain/repositories";
|
|
6
6
|
import cronCheckNetworkStatus from "../entrypoints/cron/checkNetworkStatus.cron";
|
|
7
|
-
import MIGRATIONS from "../entrypoints/db/migrations";
|
|
7
|
+
import MIGRATIONS, { DATABASE_TABLES } from "../entrypoints/db/migrations";
|
|
8
8
|
import { ActivateDomainSubscriber } from "../entrypoints/queue/activateDomain.subscriber";
|
|
9
9
|
import { NewAppSettingsSubscriber } from "../entrypoints/queue/newAppSettings.handler";
|
|
10
10
|
import { PrintImageSubscriber } from "../entrypoints/queue/printImage.subscriber";
|
|
@@ -36,5 +36,10 @@ export class BaseModule extends DomainModule {
|
|
|
36
36
|
crons() {
|
|
37
37
|
return [cronCheckNetworkStatus];
|
|
38
38
|
}
|
|
39
|
+
// Both telemetry tables survive restartDatabase() — data generated during
|
|
40
|
+
// logout must still be delivered; the flush cron runs independently of auth state.
|
|
41
|
+
persistOnReset() {
|
|
42
|
+
return [DATABASE_TABLES.TELEMETRY_QUEUE, DATABASE_TABLES.SPANS_QUEUE];
|
|
43
|
+
}
|
|
39
44
|
}
|
|
40
45
|
export const baseModule = new BaseModule();
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
+
import type { TelemetryConfig } from "../infrastructure/telemetry";
|
|
2
|
+
import type { CronWorkerOpts } from "../infrastructure/workers";
|
|
1
3
|
import type { DomainModule } from "./domain_module";
|
|
2
4
|
import { Kernel } from "./kernel";
|
|
3
5
|
export interface AppConfig {
|
|
4
6
|
domains?: DomainModule[];
|
|
7
|
+
/**
|
|
8
|
+
* Opt-in telemetry. When provided, the Loki client and flush cron start automatically
|
|
9
|
+
* after migrations complete — so the `telemetry_queue` table is guaranteed to exist.
|
|
10
|
+
*/
|
|
11
|
+
telemetry?: TelemetryConfig;
|
|
12
|
+
/**
|
|
13
|
+
* Override display options for any cron by taskName.
|
|
14
|
+
* Useful for showing/hiding toasts on internal crons (CHECK_NETWORK, TELEMETRY_FLUSH, etc.).
|
|
15
|
+
* @example
|
|
16
|
+
* crons: { CHECK_NETWORK: { showToast: true, label: "Verificando conexión" } }
|
|
17
|
+
*/
|
|
18
|
+
crons?: Record<string, Partial<CronWorkerOpts>>;
|
|
5
19
|
}
|
|
6
20
|
export declare function createApp(config?: AppConfig): Promise<Kernel>;
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
import { initTelemetry } from "../infrastructure/telemetry";
|
|
1
2
|
import { baseModule } from "./base_module";
|
|
2
3
|
import { Kernel } from "./kernel";
|
|
3
4
|
import { orchestrator } from "./orchestrator";
|
|
4
5
|
export async function createApp(config = {}) {
|
|
5
6
|
const kernel = new Kernel([baseModule, ...(config.domains ?? [])]);
|
|
7
|
+
if (config.crons)
|
|
8
|
+
kernel.applyCronConfig(config.crons);
|
|
6
9
|
orchestrator.configure(kernel);
|
|
7
10
|
await orchestrator.bootstrap();
|
|
11
|
+
if (config.telemetry) {
|
|
12
|
+
await initTelemetry(config.telemetry);
|
|
13
|
+
}
|
|
8
14
|
return kernel;
|
|
9
15
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IMigration } from "../domain/database";
|
|
2
2
|
import type { Subscriber } from "../domain/event_sourcing";
|
|
3
|
-
import type { CronWorker } from "../infrastructure/workers";
|
|
3
|
+
import type { CronWorker, CronWorkerOpts } from "../infrastructure/workers";
|
|
4
4
|
import type { DomainModule } from "./domain_module";
|
|
5
5
|
export declare class Kernel {
|
|
6
6
|
private readonly modules;
|
|
@@ -12,6 +12,7 @@ export declare class Kernel {
|
|
|
12
12
|
persistedTables(): string[];
|
|
13
13
|
subscribersByKey(): Record<string, Subscriber[]>;
|
|
14
14
|
cronsByKey(): Record<string, CronWorker[]>;
|
|
15
|
+
applyCronConfig(config: Record<string, Partial<CronWorkerOpts>>): void;
|
|
15
16
|
runMigrations(): Promise<void>;
|
|
16
17
|
bootstrap(): Promise<void>;
|
|
17
18
|
restartDatabase(): Promise<void>;
|
package/dist/framework/kernel.js
CHANGED
|
@@ -36,6 +36,14 @@ export class Kernel {
|
|
|
36
36
|
}
|
|
37
37
|
return map;
|
|
38
38
|
}
|
|
39
|
+
applyCronConfig(config) {
|
|
40
|
+
for (const crons of Object.values(this.cronsByKey())) {
|
|
41
|
+
for (const cron of crons) {
|
|
42
|
+
if (config[cron.taskName])
|
|
43
|
+
cron.configure(config[cron.taskName]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
39
47
|
async runMigrations() {
|
|
40
48
|
for (const { name, migrationFn } of this.migrations()) {
|
|
41
49
|
logger.info(`Running migration: ${name}`);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { EBuiltinCron } from "./entrypoints/cron/EBuiltinCron";
|
|
1
2
|
export type { AppShellConfig } from "./entrypoints/ui/AppShell";
|
|
2
3
|
export { createAppShell } from "./entrypoints/ui/AppShell";
|
|
3
4
|
export type { TimezoneLocale } from "./entrypoints/ui/common_provider";
|
|
@@ -13,3 +14,9 @@ export { DomainModule } from "./framework/domain_module";
|
|
|
13
14
|
export { Kernel } from "./framework/kernel";
|
|
14
15
|
export { orchestrator } from "./framework/orchestrator";
|
|
15
16
|
export { installGlobalErrorHandler } from "./infrastructure/ui/errorHandler";
|
|
17
|
+
export type { InterceptorHooks, MethodCall, } from "./infrastructure/interceptor/intercept";
|
|
18
|
+
export { interceptClass, interceptFunction, interceptInstance, } from "./infrastructure/interceptor/intercept";
|
|
19
|
+
export type { Logger } from "./infrastructure/interceptor/logging_hooks";
|
|
20
|
+
export { loggingHooks } from "./infrastructure/interceptor/logging_hooks";
|
|
21
|
+
export type { LokiAuth, LokiConfig, TelemetryConfig, } from "./infrastructure/telemetry";
|
|
22
|
+
export { initTelemetry } from "./infrastructure/telemetry";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { EBuiltinCron } from "./entrypoints/cron/EBuiltinCron";
|
|
1
2
|
export { createAppShell } from "./entrypoints/ui/AppShell";
|
|
2
3
|
export { CommonProvider, useCommon } from "./entrypoints/ui/common_provider";
|
|
3
4
|
export { ActiveDomainApp, DomainSwitcherProvider, useDomainSwitcher, } from "./entrypoints/ui/domain_switcher";
|
|
@@ -8,3 +9,6 @@ export { DomainModule } from "./framework/domain_module";
|
|
|
8
9
|
export { Kernel } from "./framework/kernel";
|
|
9
10
|
export { orchestrator } from "./framework/orchestrator";
|
|
10
11
|
export { installGlobalErrorHandler } from "./infrastructure/ui/errorHandler";
|
|
12
|
+
export { interceptClass, interceptFunction, interceptInstance, } from "./infrastructure/interceptor/intercept";
|
|
13
|
+
export { loggingHooks } from "./infrastructure/interceptor/logging_hooks";
|
|
14
|
+
export { initTelemetry } from "./infrastructure/telemetry";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ContextKey } from "./context_key";
|
|
2
|
+
/**
|
|
3
|
+
* Immutable execution-scoped key-value store.
|
|
4
|
+
*
|
|
5
|
+
* Every modification returns a new Context — the original is never mutated.
|
|
6
|
+
* This makes it safe to pass across async boundaries without defensive copies.
|
|
7
|
+
*/
|
|
8
|
+
export declare class Context {
|
|
9
|
+
private readonly _map;
|
|
10
|
+
private constructor();
|
|
11
|
+
static readonly ROOT: Context;
|
|
12
|
+
get<T>(key: ContextKey<T>): T | undefined;
|
|
13
|
+
set<T>(key: ContextKey<T>, value: T): Context;
|
|
14
|
+
delete<T>(key: ContextKey<T>): Context;
|
|
15
|
+
describe(): string;
|
|
16
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Immutable execution-scoped key-value store.
|
|
3
|
+
*
|
|
4
|
+
* Every modification returns a new Context — the original is never mutated.
|
|
5
|
+
* This makes it safe to pass across async boundaries without defensive copies.
|
|
6
|
+
*/
|
|
7
|
+
export class Context {
|
|
8
|
+
_map;
|
|
9
|
+
constructor(map) {
|
|
10
|
+
this._map = map;
|
|
11
|
+
}
|
|
12
|
+
static ROOT = new Context(new Map());
|
|
13
|
+
get(key) {
|
|
14
|
+
return this._map.get(key._symbol);
|
|
15
|
+
}
|
|
16
|
+
set(key, value) {
|
|
17
|
+
const next = new Map(this._map);
|
|
18
|
+
next.set(key._symbol, value);
|
|
19
|
+
return new Context(next);
|
|
20
|
+
}
|
|
21
|
+
delete(key) {
|
|
22
|
+
const next = new Map(this._map);
|
|
23
|
+
next.delete(key._symbol);
|
|
24
|
+
return new Context(next);
|
|
25
|
+
}
|
|
26
|
+
describe() {
|
|
27
|
+
const parts = [];
|
|
28
|
+
for (const [sym, val] of this._map) {
|
|
29
|
+
const name = sym.description ?? "unknown";
|
|
30
|
+
const repr = val !== null && typeof val === "object"
|
|
31
|
+
? (val.constructor?.name ?? "Object")
|
|
32
|
+
: String(val);
|
|
33
|
+
parts.push(`${name}: ${repr}`);
|
|
34
|
+
}
|
|
35
|
+
return `Context { ${parts.join(", ") || "empty"} }`;
|
|
36
|
+
}
|
|
37
|
+
}
|