Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7328be6997 | |||
| d32299ea18 | |||
| cd5c79c253 | |||
| 5f19786ce9 | |||
| b1b0e91616 | |||
| 582a286baa | |||
| b266019ea8 | |||
| 463c82bfcf | |||
| 6d820bf0d7 | |||
| ad24cb36fa | |||
| 6ee4e3bdf4 | |||
| 1573273d27 | |||
| d56de4ace9 | |||
| 888ce3f35f | |||
| 1c28f3155f | |||
| 31ae38853d | |||
| 2f587e22be | |||
| a248be04ca | |||
| e919945d1e | |||
| 9fd51c5a41 | |||
| 8ae4d0effd | |||
| d7c2931c10 | |||
| 11105fac28 | |||
| d3eea1aa12 | |||
| 11e5247fbf | |||
| ee658eb862 | |||
| fe512637d4 | |||
| c71fb2d8fe | |||
| 35bd1bb226 | |||
| 320699e61a | |||
| 0d9ec5c490 | |||
| 487bcbf0c1 | |||
| f12e67bdc7 | |||
| f3ed86e54c | |||
| 0a8ce6e5f9 | |||
| 9ee3cd4a53 | |||
| b8d46fbb2b | |||
| b5554a9f9c | |||
| 8adf1bda3f | |||
| dc8863a46d | |||
| bd376dc479 | |||
| dc126497db | |||
| 8ea261d3a4 | |||
| 84ef8bb1f1 | |||
| 4ec80889c2 | |||
| 002d08ba5f | |||
| a7ac30f39b | |||
| cd7cf2c4c6 | |||
| 8d8b6b9bcb | |||
| 02012f105a | |||
| 09c91172ae | |||
| 8a0717c839 | |||
| f3c4169752 | |||
| eedad4b81e | |||
| 4e39ddd5d0 | |||
| c99cb048bf | |||
| 8c4b458724 | |||
| 792560191c | |||
| c884ce88ba | |||
| 4bcd942928 | |||
| bf394e0e4b | |||
| ec94001ab8 | |||
| 505cc8c3f6 | |||
| 9fc30d3ab9 | |||
| 887664f3c0 | |||
| ae712eae64 | |||
| a3df8f6ef5 | |||
| 1006079654 | |||
| 6721a4142a | |||
| 7bf938c19f | |||
| 5932250314 | |||
| b2068e9ef2 | |||
| 2ca56dbaf4 |
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,8 +1,32 @@
|
||||
<!-- file: CHANGELOG.md -->
|
||||
<!-- version: 33 -->
|
||||
<!-- version: 35 -->
|
||||
|
||||
# Changelog KSP
|
||||
|
||||
## 0.3.15 — Raw Transaction Ingest Desk multi-route et fermeture live Mainnet — 2026-09-19
|
||||
|
||||
`0.3.15` livre `ksp-app-raw-transaction-ingest-desk` comme Desk Tauri KSP de composition et supervision des routes live `RawTransaction`. La Desk raisonne en routes complètes plutôt qu’en endpoints : une route n’est composable que lorsque toutes ses capabilities Config/Transport requises existent sur le même réseau, puis le Start reconstruit les ressources backend et lance une instance `ksp-worker-raw-transaction-ingest-lib` indépendante par route sélectionnée. Plusieurs routes peuvent partager le même Store sans partager implicitement lifecycle, gaps, `TargetCoverage` ou réparation. L’application expose inventaire, Start/Stop ciblé, lifecycle, health, activité, backpressure, reconnect/replay, gaps/repair et compteurs sûrs sans déplacer Transport, Store ou logique d’ingestion dans le frontend.
|
||||
|
||||
Les cinq familles live du Worker sont composables selon les capacités réellement déclarées : Yellowstone hydraté, Standard Logs hydraté, Standard Block direct lorsqu’un `blockSubscribe` compatible existe, Helius Transaction hydraté et HTTP Block Polling. Sur Mainnet, Yellowstone/PublicNode est traité comme capability du réseau canonique `mainnet`, pas comme réseau séparé. Le chemin Yellowstone Block final utilise le stream gRPC comme déclencheur de bloc puis un HTTP `getBlock` `Full/Base64` avec `maxSupportedTransactionVersion = 1`, au lieu d’un fan-out `getTransaction` par transaction. La queue d’updates Yellowstone reste bornée mais applique désormais une backpressure asynchrone vers le stream au lieu de transformer une saturation locale prolongée en `grpc_backpressure_overflow`; le Stop explicite accepte aussi comme fermeture coopérative un `Status` provider reçu après le half-close local, sans masquer un `grpc_status` sur session active.
|
||||
|
||||
Le live multi-provider a également révélé un cas de convergence RAW où deux acquisitions de la même transaction, du même slot et du même bloc différaient uniquement par `meta.logMessages`, PublicNode renvoyant un marqueur explicite `Log truncated`. Store journalise désormais des diagnostics bornés et sans matériau sensible, puis accepte le cas strictement prouvé **canonique complet déjà stocké + observation entrante tronquée compatible** comme observation supplémentaire sans remplacer le canonique ni arrêter le Worker. Cette exception reste volontairement asymétrique en `0.3.15` : un canonique tronqué n’est pas encore promu automatiquement vers une version complète et toute autre divergence non prouvée conserve la sémantique `content_conflict`.
|
||||
|
||||
Le gate technique final `pre.016` passe format, audits Rust/Markdown, `cargo check --workspace`, Clippy workspace/all-targets/all-features avec `-D warnings`, les suites Store PostgreSQL, Transport, Worker, Raw Transaction Ingest Desk et le rejeu `cargo test --workspace --all-targets --all-features`, ainsi que les graphes Cargo et `cargo tree --duplicates`. `cargo tauri build` produit les trois bundles Linux `.deb`, `.rpm` et `.AppImage`. Le live de référence immédiatement antérieur fait tourner Yellowstone Mainnet et HTTP Block Polling environ dix-neuf minutes en parallèle sans `grpc_backpressure_overflow`, sans terminal `content_conflict` et sans route `Faulted`; les deux routes terminent ensuite `Stopped/Healthy`. La réconciliation documentaire `pre.017` repasse ensuite format, audits Rust/Markdown et `cargo check --workspace` sans réouvrir le runtime.
|
||||
|
||||
`prompts/035-V0_3_16_START_PROMPT.md` ouvre `0.3.16` exclusivement depuis le futur tag stable `v0.3.15`. Cette release est dédiée à la résilience RAW et à la gestion durable des conflits : variantes conservées, comparaison `Exact / CompatibleLessComplete / CompatibleMoreComplete / Conflict`, promotions réversibles, quarantaine non terminale des conflits, historique de résolution, Store retry/backpressure, reconnexion Transport configurable et nouvelles vues de `ksp-app-store-desk`. `0.3.17` reste réservé au Backfill multi-route/multi-stratégie et `0.3.18` à l’adaptation de `ksp-app-backfill-desk`.
|
||||
|
||||
## 0.3.14 — gap repair et hardening multi-source du Worker RawTransaction — 2026-09-12
|
||||
|
||||
`0.3.14` ferme `ksp-worker-raw-transaction-ingest-lib` par une continuité run-local explicite au-dessus des cinq familles live de `0.3.13`, sans transformer le Worker en moteur historique. Le Worker distingue désormais reconnect, tentative de replay, delivery de replay et preuve de coverage ; il maintient des gaps bornés avec plage inclusive, `TargetCoverage` conservative et frontier de continuité distincte de la processing frontier. Une reprise live ou l'acceptation d'un `from_slot` ne vaut jamais à elle seule preuve de réparation.
|
||||
|
||||
La réconciliation peut s'appuyer sur une coverage réellement prouvée d'une source redondante, un scan HTTP borné de blocs/slots ou l'hydration `getTransaction observed` des références connues. Les bornes finales restent 64 gaps ouverts, 4096 slots par plage, fenêtres de discovery de 512 slots, 4 `getBlock` logiques en vol et un seul gap activement réparé. Les matériaux de repair repassent par la même canonicalisation Common RAW, la même admission, la même hydration globale et le même Store ; aucun second pipeline, retry réseau Worker, Job Backfill automatique ou backend Store direct n'est introduit.
|
||||
|
||||
La politique multi-source ne fault plus mécaniquement sur toute perte de source : la continuation n'est autorisée que lorsque les gaps concernés sont réconciliés et que la coverage présente/future requise est réellement démontrée. Les snapshots publics ajoutent une observabilité source-neutral et bornée des gaps avec états/raisons/méthodes et compteurs checked, sans source key, endpoint, token, signature, payload ou erreur provider arbitraire. La health policy reste conservative, la fairness alterne trafic nominal/repair sur les capacités existantes et le shutdown borne également le drain interne des sources avec abort+join des siblings non coopératifs.
|
||||
|
||||
La fermeture cross-layer confirme le pipeline unique `Transport -> Worker -> ksp-raw-transaction-lib -> Store`, la non-régression Legacy/V0/V1, les dependency firewalls et l'indépendance Worker/Backfill. Le gate technique final passe fmt, audits Rust/Markdown, `cargo check --workspace`, Clippy workspace/all-targets/all-features avec `-D warnings`, `cargo test --workspace --all-targets --all-features` et les graphes Cargo : `1 931` tests passent, `0` échoue et `15` restent ignored opt-in/operator-only. Les smokes keyless Solana HTTP Devnet et WebSocket Devnet passent `1/1` chacun ; Yellowstone/provider-gated, Helius live, PostgreSQL live et Worker->Store end-to-end restent explicitement non exécutés lorsqu'aucune ressource correspondante n'a été provisionnée.
|
||||
|
||||
`prompts/034-V0_3_15_START_PROMPT.md` ouvre `0.3.15` exclusivement depuis le futur tag stable `v0.3.14`. Cette release introduit `ksp-app-raw-transaction-ingest-desk` comme Desk Tauri de composition/supervision : une **route** est une stratégie complète pouvant nécessiter plusieurs capacités Config/Transport, elle n'est sélectionnable que si toutes ses requirements sont composables, puis une instance Worker réelle est démarrée par route sélectionnée et valide l'opérabilité runtime au Start. Le frontend reste une surface sûre ; Config, Transport, Store et Worker conservent leurs responsabilités.
|
||||
|
||||
## 0.3.13 — convergence live multi-source du Worker RawTransaction — 2026-09-10
|
||||
|
||||
`0.3.13` généralise `ksp-worker-raw-transaction-ingest-lib` de la verticale Yellowstone de `0.3.12` vers une composition caller-owned de `1..32` sources logiques appartenant au même réseau. Cinq familles live convergent désormais vers le même pipeline Common RAW / admission / Store : Yellowstone, Standard Logs avec hydration `getTransaction observed`, Standard Block RAW-direct, Helius `transactionSubscribe` avec hydration et HTTP Block Polling run-local RAW-direct. Le Worker reste sans Config, Job Backfill, backend Store physique, client réseau direct ou SDK provider parallèle.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 565
|
||||
# version: 637
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api", "crates/ksp-worker-raw-transaction-ingest-lib"]
|
||||
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-raw-transaction-ingest-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api", "crates/ksp-worker-raw-transaction-ingest-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.13"
|
||||
version = "0.3.15"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: README.md -->
|
||||
<!-- version: 14 -->
|
||||
<!-- version: 15 -->
|
||||
|
||||
# Khadhroony Solana Project
|
||||
|
||||
@@ -57,7 +57,9 @@ La couche RAW dispose d'une façade Store backend-neutral et d'un premier job hi
|
||||
|
||||
`ksp-worker-api` fournit désormais la fondation générique des services continus : identité Worker, lifecycle borné, health/activity, intention de stop coopératif et observation latest-value. Cette API reste Core-only, runtime-neutral et sans connaissance Solana, Transport, Store ou Job. Elle ne démarre ni n'arrête elle-même un runtime concret.
|
||||
|
||||
`ksp-worker-raw-transaction-ingest-lib` est le premier Worker concret distinct du Job Backfill. Il conserve sa fondation Start/Stop, admission bornée, Common RAW, Store backend-neutral, shutdown borné et snapshots latest-value. Ses sources productives couvrent maintenant Yellowstone + hydration HTTP `getTransaction`, Standard WS `logsSubscribe` + hydration, Standard WS `blockSubscribe` RAW-direct, Helius `transactionSubscribe` + hydration et Solana HTTP live block polling. Le polling HTTP démarre au `getSlot` observé du run, découvre uniquement les blocs live via `getBlocksWithLimit` puis matérialise les slots listés via `getBlock observed`; il n'est jamais utilisé comme Backfill implicite. Les signaux hydratés de même `(network, signature, commitment)` convergent vers le même coordinator, tandis que les voies block RAW-direct passent par la même admission Common RAW. Le reconnect/replay reste propriétaire de Transport, la frontier de traitement reste run-local et un gap de rétention prouvé provoque un fault au lieu de lancer une campagne historique. `ksp-job-backfill-lib` conserve son rôle historique paramétré et borné ; les deux producteurs restent indépendants et convergent uniquement vers les mêmes contrats RAW/Store.
|
||||
`ksp-worker-raw-transaction-ingest-lib` est le premier Worker concret distinct du Job Backfill. Il conserve sa fondation Start/Stop, admission bornée, Common RAW, Store backend-neutral, shutdown borné et snapshots latest-value. Ses cinq familles productives couvrent Yellowstone, Standard WS `logsSubscribe`, Standard WS `blockSubscribe`, Helius `transactionSubscribe` et Solana HTTP live block polling. Les signaux transactionnels Yellowstone/Standard Logs/Helius utilisent l'hydration HTTP `getTransaction`, tandis que le mode Yellowstone Block utilisé par la route Mainnet du Desk emploie le bloc gRPC comme trigger puis réconcilie le slot par HTTP `getBlock` Full/Base64 avec concurrence bornée. Standard Block et HTTP Block Polling qualifient directement leurs transactions Legacy/V0/V1 vers Common RAW. Les queues restent bornées ; une saturation durable de la livraison Yellowstone applique désormais une backpressure asynchrone au stream gRPC au lieu de produire un overflow local. Le Store accepte aussi l'exception étroite déjà prouvée où un canonique complet reçoit ensuite une observation identique hors `logMessages` explicitement tronqués ; le canonique complet est conservé et les autres divergences restent fail-closed en `0.3.15`. Le reconnect/replay reste propriétaire de Transport, la frontier de traitement reste run-local et un gap de rétention prouvé provoque un fault au lieu de lancer une campagne historique. `ksp-job-backfill-lib` conserve son rôle historique paramétré et borné ; les deux producteurs restent indépendants et convergent uniquement vers les mêmes contrats RAW/Store.
|
||||
|
||||
`ksp-app-raw-transaction-ingest-desk` compose et supervise ces routes live sans déplacer leurs responsabilités dans l'UI. Les profils applicatifs sont réseau-centriques, plusieurs routes d'un même réseau peuvent partager un Store tout en gardant des Workers indépendants, et le monitoring latest-value expose lifecycle, health, activité, persistence, reconnect/replay, continuité, gaps et repair sans URL, secret, payload RAW ou handle physique. Le gate `0.3.15` valide notamment le fonctionnement parallèle Mainnet de Yellowstone Block + HTTP `getBlock` et HTTP Block Polling, la fermeture coopérative de Yellowstone après half-close et le build Tauri Linux de la Desk.
|
||||
|
||||
## Points d'entrée
|
||||
|
||||
|
||||
12
ROADMAP.md
12
ROADMAP.md
@@ -1,5 +1,5 @@
|
||||
<!-- file: ROADMAP.md -->
|
||||
<!-- version: 111 -->
|
||||
<!-- version: 114 -->
|
||||
|
||||
# Roadmap KSP
|
||||
|
||||
@@ -106,9 +106,11 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
|
||||
- [X] `0.3.11` — Fondation runtime de `ksp-worker-raw-transaction-ingest-lib` livrée : crate/dependency firewall, settings source-neutral, handle/start-stop, lifecycle, snapshots, supervisor, admission bornée, canonicalisation Common RAW, persistence/déduplication Store déterministe, backpressure et shutdown/fault hardening. La release se ferme volontairement sans source réseau productive ; Yellowstone + hydration HTTP commencent en `0.3.12`.
|
||||
- [X] `0.3.12` — Première source live productive du Worker livrée : Yellowstone `Transaction`/`TransactionStatus`/`Block` vers signaux source-neutral, coalescence bornée puis hydration HTTP `getTransaction` observed avant Common RAW/Store ; `BlockMeta`/`Slot` restent continuity-only. Processing frontier run-local, reconnect/from_slot/ReplayInfo Transport-owned, compteurs source-neutral et fault sur gap de rétention prouvé sans Backfill automatique. Hardening duplicate/backpressure/stop/fault et canaris cross-layer fermés ; gate workspace complet vert, smokes live HTTP Devnet + WebSocket Devnet verts, Yellowstone token-gated/Worker end-to-end laissés explicitement non exécutés.
|
||||
- [X] `0.3.13` — Convergence live multi-source du Worker RAW livrée : composition caller-owned `1..32` sources d’un même réseau, cinq familles Yellowstone / Standard Logs / Standard Block / Helius Transaction / HTTP Block Polling, hydration globale des trois voies reference-bearing, RAW-direct Legacy/V0/V1 pour les deux voies blocs, observations multiples, content conflict explicite, coalescence cross-source, fairness/backpressure/health source-neutral et shutdown hardening sans second actor Transport. Gate workspace complet vert (`1 850` PASS / `0` échec / `15` ignored) et smokes keyless HTTP + WebSocket Devnet verts ; les preuves provider/resource-gated non exécutées restent explicitement non revendiquées.
|
||||
- [ ] `0.3.14` — Fermer le Worker par le **gap repair/hardening multi-source** : replay natif, source redondante, scan HTTP blocs, hydration de réparation, unresolved gaps visibles, politiques degraded/unhealthy/faulted, shutdown pendant repair, smokes provider accessibles et adapter EARLY seulement si réellement prouvé.
|
||||
- [ ] `0.3.15` — Introduire `ksp-app-raw-transaction-ingest-desk`, Desk Tauri KSP spécialisée qui choisit et supervise une ou plusieurs sources/méthodes réellement admises par le Worker finalisé : lifecycle, health, rates, backpressure, reconnect/recovery, gap state et compteurs sûrs. La Desk ne réimplémente ni discovery, hydration, déduplication, reprise ni persistence.
|
||||
- [ ] `0.3.16` — Étendre `ksp-job-backfill-lib` et `ksp-app-backfill-desk` au **backfill multi-source/multi-stratégie** à partir de la même matrice d'acquisition auditée en `0.3.9`. Conserver la stratégie actuelle `getSignaturesForAddress + getTransaction` comme première voie HTTP valide, puis ajouter seulement les voies historiques/catch-up/gap-repair réellement pertinentes et sûres, sans supposer qu'une source live WS constitue un historique universel.
|
||||
- [X] `0.3.14` — Gap repair/hardening multi-source du Worker RAW livré : gaps run-local bornés, distinction reconnect/replay/delivery/coverage, TargetCoverage conservative, coverage redondante prouvée, discovery HTTP bornée, known-reference hydration, réconciliation source-loss, health gap-aware, fairness nominal/repair, snapshots publics source-neutral et shutdown/drain renforcé. Le pipeline Common RAW/Store reste unique, Worker/Backfill restent indépendants, EARLY n’est pas ajouté. Gate workspace complet vert (`1 931` PASS / `0` échec / `15` ignored) et smokes keyless HTTP + WebSocket Devnet verts ; preuves provider/resource-gated non provisionnées explicitement non revendiquées.
|
||||
- [X] `0.3.15` — `ksp-app-raw-transaction-ingest-desk` livrée comme Desk Tauri KSP multi-route : composition capability-driven par réseau, une instance Worker indépendante par route sélectionnée, Store partagé, Start/Stop ciblé et supervision lifecycle/health/backpressure/reconnect/replay/gaps/repair sans réimplémenter Transport ni persistence. Les cinq familles live sont projetées selon les capacités réellement configurées ; Yellowstone Mainnet utilise le trigger gRPC Block + HTTP `getBlock` et applique une backpressure bornée sans overflow local prolongé. Le Stop Yellowstone post-half-close est coopératif, et Store accepte le cas strict `canonique complet + incoming logMessages explicitement tronqué compatible` sans remplacer le canonique ni arrêter le Worker. Gate workspace/Clippy/tests/graphes/bundles Linux vert et live Mainnet Yellowstone + HTTP polling d’environ 19 minutes fermé `Stopped/Healthy`. La généralisation variantes/conflits/promotions/retry/reconnect reste explicitement `0.3.16`.
|
||||
- [ ] `0.3.16` — **RAW resilience / conflict management** : faire converger Store API/façade/PostgreSQL, Worker live et Store Desk autour de variantes RAW durables, résolution automatique `Exact / CompatibleLessComplete / CompatibleMoreComplete / Conflict`, quarantaine des conflits non résolus sans arrêt du Worker, historique réversible des promotions/résolutions, retry Store sans perte silencieuse et reconnexion Transport configurable/bornée. Une provenance provider n'est jamais une priorité canonique en soi ; la représentation prouvée la plus complète gagne.
|
||||
- [ ] `0.3.17` — Étendre `ksp-job-backfill-lib` au **backfill multi-route/multi-stratégie** à partir de la matrice d'acquisition auditée en `0.3.9` et de la convergence Store `0.3.16`. Conserver `getSignaturesForAddress + getTransaction` comme première voie valide puis ajouter les stratégies historiques/catch-up réellement pertinentes sans edge Job ↔ Worker.
|
||||
- [ ] `0.3.18` — Adapter `ksp-app-backfill-desk` au Job multi-route `0.3.17` : inventaire/composition de routes, sélection/supervision et progression sur le modèle structurel de `ksp-app-raw-transaction-ingest-desk`, sans réimplémenter la logique du Job.
|
||||
|
||||
### TODO/IDEAS — applications spécialisées et control plane
|
||||
|
||||
@@ -118,7 +120,7 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
|
||||
### TODO/IDEAS — taxonomie D1, processing et rétention
|
||||
|
||||
- [ ] **TODO** — maintenir la matrice d’admission HTTP/WS/gRPC/provider lors de toute nouvelle famille D1 : plusieurs sources ne convergent vers un même struct que si elles satisfont la même sémantique sans perte.
|
||||
- [X] **TODO `0.3.9`** — matrice RAW Transaction unique produite et consolidée dans `docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md`, avec séparation possibilités/support/preuve, capabilities orthogonales aux producteurs, provenance/déduplication/continuité et handoffs désormais scindés `0.3.11`–`0.3.14` Worker live / `0.3.16` Backfill historique.
|
||||
- [X] **TODO `0.3.9`** — matrice RAW Transaction unique produite et consolidée dans `docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md`, avec séparation possibilités/support/preuve, capabilities orthogonales aux producteurs, provenance/déduplication/continuité et handoffs désormais scindés `0.3.11`–`0.3.14` Worker live / `0.3.16` résilience RAW / `0.3.17`–`0.3.18` Backfill multi-route.
|
||||
- [X] **TODO Helius `0.3.13`** — la voie Worker Helius `transactionSubscribe` réutilise la surface Transport et les profils Config Helius déjà existants avec `KSP_SECRET_HELIUS_API_KEY`; aucun nouveau profil, secret, SDK provider, tier codé ou second client parallèle n’a été nécessaire. Le payload riche reste Transport-owned et la voie Worker hydrate via `getTransaction observed` tant que le RAW complet n’est pas prouvé directement.
|
||||
- [X] **TODO réseau** — identité KSP canonique fixée à `mainnet` dans Config/Store/Transport/tests ; `mainnet-beta` reste uniquement un alias legacy/externe lorsque la frontière provider l'exige. Les données D1 RAW antérieures restent non autoritaires pendant cette phase et aucune compatibilité de base de test n'impose l'ancien libellé.
|
||||
- [X] `RawAccountState` + observation — contrat commun stabilisé en `0.3.1` avec bytes complets + slot, provenance séparée et enrichissements source-specific optionnels ; la persistence PostgreSQL physique est complétée en `0.3.4` avec les quatre capabilities account et la conformance RAW 10/10.
|
||||
|
||||
91
config/composite.ksp-app-raw-transaction-ingest-desk.json
Normal file
91
config/composite.ksp-app-raw-transaction-ingest-desk.json
Normal file
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"default_profile": "devnet",
|
||||
"profiles": [
|
||||
{
|
||||
"profile_id": "devnet",
|
||||
"documents": [
|
||||
{
|
||||
"component_id": "logging",
|
||||
"file_id": "cfg.std.logging",
|
||||
"profile_id": "supertrace"
|
||||
},
|
||||
{
|
||||
"component_id": "transport",
|
||||
"file_id": "cfg.std.transport",
|
||||
"profile_id": "devnet_public"
|
||||
},
|
||||
{
|
||||
"component_id": "transport.helius",
|
||||
"file_id": "cfg.std.transport",
|
||||
"profile_id": "helius_devnet"
|
||||
},
|
||||
{
|
||||
"component_id": "transport.yellowstone",
|
||||
"file_id": "cfg.std.transport",
|
||||
"profile_id": "orbitflare_devnet"
|
||||
},
|
||||
{
|
||||
"component_id": "store",
|
||||
"file_id": "cfg.std.store",
|
||||
"profile_id": "devnet"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"profile_id": "mainnet",
|
||||
"documents": [
|
||||
{
|
||||
"component_id": "logging",
|
||||
"file_id": "cfg.std.logging",
|
||||
"profile_id": "supertrace"
|
||||
},
|
||||
{
|
||||
"component_id": "transport",
|
||||
"file_id": "cfg.std.transport",
|
||||
"profile_id": "mainnet_public"
|
||||
},
|
||||
{
|
||||
"component_id": "transport.helius",
|
||||
"file_id": "cfg.std.transport",
|
||||
"profile_id": "helius_mainnet"
|
||||
},
|
||||
{
|
||||
"component_id": "transport.yellowstone",
|
||||
"file_id": "cfg.std.transport",
|
||||
"profile_id": "publicnode_mainnet"
|
||||
},
|
||||
{
|
||||
"component_id": "store",
|
||||
"file_id": "cfg.std.store",
|
||||
"profile_id": "mainnet"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"profile_id": "testnet",
|
||||
"documents": [
|
||||
{
|
||||
"component_id": "logging",
|
||||
"file_id": "cfg.std.logging",
|
||||
"profile_id": "supertrace"
|
||||
},
|
||||
{
|
||||
"component_id": "transport",
|
||||
"file_id": "cfg.std.transport",
|
||||
"profile_id": "testnet_public"
|
||||
},
|
||||
{
|
||||
"component_id": "transport.yellowstone",
|
||||
"file_id": "cfg.std.transport",
|
||||
"profile_id": "publicnode_testnet"
|
||||
},
|
||||
{
|
||||
"component_id": "store",
|
||||
"file_id": "cfg.std.store",
|
||||
"profile_id": "testnet"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -87,7 +87,15 @@
|
||||
"provider": "solana-public",
|
||||
"cluster": "mainnet",
|
||||
"kind": "solana_standard",
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}"
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}",
|
||||
"capabilities": [
|
||||
"account",
|
||||
"logs",
|
||||
"program",
|
||||
"root",
|
||||
"signature",
|
||||
"slot"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "mainnet_helius_ws",
|
||||
@@ -99,7 +107,17 @@
|
||||
"session": {
|
||||
"notification_queue_capacity": 512,
|
||||
"max_active_subscriptions": 2048
|
||||
}
|
||||
},
|
||||
"capabilities": [
|
||||
"account",
|
||||
"logs",
|
||||
"program",
|
||||
"root",
|
||||
"signature",
|
||||
"slot",
|
||||
"slots_updates",
|
||||
"helius_transaction"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -140,7 +158,17 @@
|
||||
"provider": "helius",
|
||||
"cluster": "devnet",
|
||||
"kind": "helius_laserstream",
|
||||
"url": "wss://devnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}"
|
||||
"url": "wss://devnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}",
|
||||
"capabilities": [
|
||||
"account",
|
||||
"logs",
|
||||
"program",
|
||||
"root",
|
||||
"signature",
|
||||
"slot",
|
||||
"slots_updates",
|
||||
"helius_transaction"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -348,6 +348,26 @@
|
||||
},
|
||||
"session": {
|
||||
"$ref": "#/$defs/wsSessionOverride"
|
||||
},
|
||||
"capabilities": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 10,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"enum": [
|
||||
"account",
|
||||
"block",
|
||||
"logs",
|
||||
"program",
|
||||
"root",
|
||||
"signature",
|
||||
"slot",
|
||||
"slots_updates",
|
||||
"vote",
|
||||
"helius_transaction"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -75,7 +75,67 @@
|
||||
"provider": "solana-public",
|
||||
"cluster": "devnet",
|
||||
"kind": "solana_standard",
|
||||
"url": "${KSP_PUBLIC_SOLANA_DEVNET_WS_URL:-wss://api.devnet.solana.com}"
|
||||
"url": "${KSP_PUBLIC_SOLANA_DEVNET_WS_URL:-wss://api.devnet.solana.com}",
|
||||
"capabilities": [
|
||||
"account",
|
||||
"block",
|
||||
"logs",
|
||||
"program",
|
||||
"root",
|
||||
"signature",
|
||||
"slot"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"profile_id": "helius_devnet",
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "solana_devnet_public",
|
||||
"enabled": true,
|
||||
"provider": "solana-public",
|
||||
"cluster": "devnet",
|
||||
"url": "${KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL:-https://api.devnet.solana.com}",
|
||||
"connect_timeout_ms": 5000,
|
||||
"request_timeout_ms": 15000,
|
||||
"max_idle_connections_per_host": 8,
|
||||
"roles": [
|
||||
{
|
||||
"role": "default",
|
||||
"enabled": true,
|
||||
"request_kinds": [
|
||||
"*"
|
||||
],
|
||||
"priority": 100,
|
||||
"limits": {
|
||||
"requests_per_second": 5,
|
||||
"burst_capacity": 10,
|
||||
"max_concurrent_requests": 8,
|
||||
"pause_after_rate_limit_ms": 1000
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ws_endpoints": [
|
||||
{
|
||||
"name": "helius_devnet_ws",
|
||||
"enabled": true,
|
||||
"provider": "helius",
|
||||
"cluster": "devnet",
|
||||
"kind": "helius_laserstream",
|
||||
"url": "wss://devnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY}",
|
||||
"capabilities": [
|
||||
"account",
|
||||
"logs",
|
||||
"program",
|
||||
"root",
|
||||
"signature",
|
||||
"slot",
|
||||
"slots_updates",
|
||||
"helius_transaction"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -116,7 +176,16 @@
|
||||
"provider": "solana-public",
|
||||
"cluster": "devnet",
|
||||
"kind": "solana_standard",
|
||||
"url": "${KSP_PUBLIC_SOLANA_DEVNET_WS_URL:-wss://api.devnet.solana.com}"
|
||||
"url": "${KSP_PUBLIC_SOLANA_DEVNET_WS_URL:-wss://api.devnet.solana.com}",
|
||||
"capabilities": [
|
||||
"account",
|
||||
"block",
|
||||
"logs",
|
||||
"program",
|
||||
"root",
|
||||
"signature",
|
||||
"slot"
|
||||
]
|
||||
}
|
||||
],
|
||||
"grpc_endpoints": [
|
||||
@@ -174,7 +243,67 @@
|
||||
"provider": "solana-public",
|
||||
"cluster": "mainnet",
|
||||
"kind": "solana_standard",
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}"
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}",
|
||||
"capabilities": [
|
||||
"account",
|
||||
"block",
|
||||
"logs",
|
||||
"program",
|
||||
"root",
|
||||
"signature",
|
||||
"slot"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"profile_id": "helius_mainnet",
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "solana_mainnet_public",
|
||||
"enabled": true,
|
||||
"provider": "solana-public",
|
||||
"cluster": "mainnet",
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet.solana.com}",
|
||||
"connect_timeout_ms": 5000,
|
||||
"request_timeout_ms": 15000,
|
||||
"max_idle_connections_per_host": 8,
|
||||
"roles": [
|
||||
{
|
||||
"role": "default",
|
||||
"enabled": true,
|
||||
"request_kinds": [
|
||||
"*"
|
||||
],
|
||||
"priority": 100,
|
||||
"limits": {
|
||||
"requests_per_second": 5,
|
||||
"burst_capacity": 10,
|
||||
"max_concurrent_requests": 8,
|
||||
"pause_after_rate_limit_ms": 1000
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ws_endpoints": [
|
||||
{
|
||||
"name": "helius_mainnet_ws",
|
||||
"enabled": true,
|
||||
"provider": "helius",
|
||||
"cluster": "mainnet",
|
||||
"kind": "helius_laserstream",
|
||||
"url": "wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY}",
|
||||
"capabilities": [
|
||||
"account",
|
||||
"logs",
|
||||
"program",
|
||||
"root",
|
||||
"signature",
|
||||
"slot",
|
||||
"slots_updates",
|
||||
"helius_transaction"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -269,7 +398,16 @@
|
||||
"provider": "solana-public",
|
||||
"cluster": "mainnet",
|
||||
"kind": "solana_standard",
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}"
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}",
|
||||
"capabilities": [
|
||||
"account",
|
||||
"block",
|
||||
"logs",
|
||||
"program",
|
||||
"root",
|
||||
"signature",
|
||||
"slot"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -277,7 +415,31 @@
|
||||
"profile_id": "publicnode_mainnet",
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "solana_mainnet_public",
|
||||
"name": "publicnode_solana_mainnet_rpc",
|
||||
"enabled": true,
|
||||
"provider": "publicnode",
|
||||
"cluster": "mainnet",
|
||||
"url": "https://solana-rpc.publicnode.com",
|
||||
"connect_timeout_ms": 5000,
|
||||
"request_timeout_ms": 15000,
|
||||
"max_idle_connections_per_host": 8,
|
||||
"roles": [
|
||||
{
|
||||
"role": "default",
|
||||
"enabled": true,
|
||||
"request_kinds": [
|
||||
"*"
|
||||
],
|
||||
"priority": 100,
|
||||
"limits": {
|
||||
"max_concurrent_requests": 8,
|
||||
"pause_after_rate_limit_ms": 1000
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "solana_mainnet_public_repair",
|
||||
"enabled": true,
|
||||
"provider": "solana-public",
|
||||
"cluster": "mainnet",
|
||||
@@ -310,7 +472,16 @@
|
||||
"provider": "solana-public",
|
||||
"cluster": "mainnet",
|
||||
"kind": "solana_standard",
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}"
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}",
|
||||
"capabilities": [
|
||||
"account",
|
||||
"block",
|
||||
"logs",
|
||||
"program",
|
||||
"root",
|
||||
"signature",
|
||||
"slot"
|
||||
]
|
||||
}
|
||||
],
|
||||
"grpc_endpoints": [
|
||||
@@ -330,6 +501,56 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"profile_id": "testnet_public",
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "solana_testnet_public",
|
||||
"enabled": true,
|
||||
"provider": "solana-public",
|
||||
"cluster": "testnet",
|
||||
"url": "https://api.testnet.solana.com",
|
||||
"connect_timeout_ms": 5000,
|
||||
"request_timeout_ms": 15000,
|
||||
"max_idle_connections_per_host": 8,
|
||||
"roles": [
|
||||
{
|
||||
"role": "default",
|
||||
"enabled": true,
|
||||
"request_kinds": [
|
||||
"*"
|
||||
],
|
||||
"priority": 100,
|
||||
"limits": {
|
||||
"requests_per_second": 5,
|
||||
"burst_capacity": 10,
|
||||
"max_concurrent_requests": 8,
|
||||
"pause_after_rate_limit_ms": 1000
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ws_endpoints": [
|
||||
{
|
||||
"name": "solana_testnet_public_ws",
|
||||
"enabled": true,
|
||||
"provider": "solana-public",
|
||||
"cluster": "testnet",
|
||||
"kind": "solana_standard",
|
||||
"url": "wss://api.testnet.solana.com",
|
||||
"capabilities": [
|
||||
"account",
|
||||
"block",
|
||||
"logs",
|
||||
"program",
|
||||
"root",
|
||||
"signature",
|
||||
"slot"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"profile_id": "publicnode_testnet",
|
||||
"endpoints": [
|
||||
@@ -367,7 +588,16 @@
|
||||
"provider": "solana-public",
|
||||
"cluster": "testnet",
|
||||
"kind": "solana_standard",
|
||||
"url": "wss://api.testnet.solana.com"
|
||||
"url": "wss://api.testnet.solana.com",
|
||||
"capabilities": [
|
||||
"account",
|
||||
"block",
|
||||
"logs",
|
||||
"program",
|
||||
"root",
|
||||
"signature",
|
||||
"slot"
|
||||
]
|
||||
}
|
||||
],
|
||||
"grpc_endpoints": [
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
],
|
||||
"resources": {
|
||||
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
|
||||
"../../config/composite.ksp-app-raw-transaction-ingest-desk.json": "config/composite.ksp-app-raw-transaction-ingest-desk.json",
|
||||
"../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-desk.json",
|
||||
"../../config/composite.ksp-app-store-desk.json": "config/composite.ksp-app-store-desk.json",
|
||||
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-backfill-desk/tests/desktop_contract.rs
|
||||
// version: 17
|
||||
// version: 18
|
||||
|
||||
//! Structural desktop contract checks for the Backfill Desk scaffold.
|
||||
|
||||
@@ -145,9 +145,10 @@ fn pre_003_composite_packaging_follows_atomic_config_registry_contract() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert_eq!(resources.len(), 15);
|
||||
assert_eq!(resources.len(), 16);
|
||||
for required in [
|
||||
"../../config/composite.ksp-app-backfill-desk.json",
|
||||
"../../config/composite.ksp-app-raw-transaction-ingest-desk.json",
|
||||
"../../config/composite.ksp-app-solprices-desk.json",
|
||||
"../../config/composite.ksp-app-store-desk.json",
|
||||
"../../config/composite.ksp-app-wallet-desk.json",
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
],
|
||||
"resources": {
|
||||
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
|
||||
"../../config/composite.ksp-app-raw-transaction-ingest-desk.json": "config/composite.ksp-app-raw-transaction-ingest-desk.json",
|
||||
"../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-desk.json",
|
||||
"../../config/composite.ksp-app-store-desk.json": "config/composite.ksp-app-store-desk.json",
|
||||
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/tests/desktop_contract.rs
|
||||
// version: 13
|
||||
// version: 14
|
||||
|
||||
//! Desktop build/shell contract audits for Config Desk.
|
||||
|
||||
@@ -110,11 +110,15 @@ fn pre_018_packaged_runtime_bundles_config_resources_and_activates_shared_writab
|
||||
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
|
||||
assert!(resources.is_some(), "packaged Config resources map must exist");
|
||||
if let std::option::Option::Some(resources) = resources {
|
||||
assert_eq!(resources.len(), 15);
|
||||
assert_eq!(resources.len(), 16);
|
||||
assert_eq!(
|
||||
resources.get("../../config/composite.ksp-app-backfill-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-backfill-desk.json"),
|
||||
);
|
||||
assert_eq!(
|
||||
resources.get("../../config/composite.ksp-app-raw-transaction-ingest-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-raw-transaction-ingest-desk.json"),
|
||||
);
|
||||
assert_eq!(
|
||||
resources.get("../../config/composite.ksp-app-solprices-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-solprices-desk.json"),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/unit_tests/profiles.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
#[test]
|
||||
fn profile_inventory_exposes_registered_profile_documents() {
|
||||
@@ -21,6 +21,9 @@ fn profile_inventory_exposes_registered_profile_documents() {
|
||||
assert!(inventory.iter().any(|document| -> bool {
|
||||
return document.file_id == ksp_config_lib::FILE_ID_STD_WALLET;
|
||||
}));
|
||||
assert!(inventory.iter().any(|document| -> bool {
|
||||
return document.file_id == ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK;
|
||||
}));
|
||||
assert!(inventory.iter().any(|document| -> bool {
|
||||
return document.file_id == ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK;
|
||||
}));
|
||||
|
||||
44
crates/ksp-app-raw-transaction-ingest-desk/Cargo.toml
Normal file
44
crates/ksp-app-raw-transaction-ingest-desk/Cargo.toml
Normal file
@@ -0,0 +1,44 @@
|
||||
# file: crates/ksp-app-raw-transaction-ingest-desk/Cargo.toml
|
||||
# version: 4
|
||||
|
||||
[package]
|
||||
name = "ksp-app-raw-transaction-ingest-desk"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "ksp_app_raw_transaction_ingest_desk_lib"
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[[bin]]
|
||||
name = "ksp-app-raw-transaction-ingest-desk"
|
||||
path = "src/main.rs"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build.workspace = true
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true, features = ["std", "now"] }
|
||||
fs2.workspace = true
|
||||
ksp-config-lib = { path = "../ksp-config-lib" }
|
||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||
ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
|
||||
ksp-store-lib = { path = "../ksp-store-lib" }
|
||||
ksp-worker-api = { path = "../ksp-worker-api" }
|
||||
ksp-worker-raw-transaction-ingest-lib = { path = "../ksp-worker-raw-transaction-ingest-lib" }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tauri.workspace = true
|
||||
tauri-plugin-tracing.workspace = true
|
||||
tokio = { workspace = true, features = ["time"] }
|
||||
ts-rs.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
76
crates/ksp-app-raw-transaction-ingest-desk/README.md
Normal file
76
crates/ksp-app-raw-transaction-ingest-desk/README.md
Normal file
@@ -0,0 +1,76 @@
|
||||
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/README.md -->
|
||||
<!-- version: 15 -->
|
||||
|
||||
# `ksp-app-raw-transaction-ingest-desk`
|
||||
|
||||
Application desktop Tauri KSP destinée à composer et superviser les routes live alimentant le Store commun en `RawTransaction`.
|
||||
|
||||
Le composant conserve les frontières KSP : Config possède les profils/endpoints/secrets, Transport les clients et sessions, Store la persistance et le Worker la logique d'ingestion. Le shell desktop ne déplace jamais ces responsabilités vers le frontend.
|
||||
|
||||
## Package
|
||||
|
||||
```text
|
||||
package : ksp-app-raw-transaction-ingest-desk
|
||||
lib : ksp_app_raw_transaction_ingest_desk_lib
|
||||
bin : ksp-app-raw-transaction-ingest-desk
|
||||
```
|
||||
|
||||
## Frontières
|
||||
|
||||
```text
|
||||
Config -> composite, profils, secrets et résolution effective
|
||||
Transport -> capabilities HTTP/WS/gRPC et ressources physiques
|
||||
Desk -> catalogue logique, revalidation Start et ownership du lifecycle mono/multi-route
|
||||
Store -> persistance via la façade KSP
|
||||
Worker -> validation des contrats source, acquisition continue et lifecycle d'exécution
|
||||
```
|
||||
|
||||
L'inventaire de routes est calculé côté Rust à partir du composite Config validé et des settings Transport/Store résolus. Il ne lance aucun probe réseau et n'ouvre ni Store ni Worker.
|
||||
|
||||
Au Start, le backend revalide une sélection logique par `profile_id + route_id + inventory_generation + commitment`. Il recharge Config, reproouve le réseau Store/Transport, reconstruit le pool HTTP ou l'endpoint WS/gRPC exact requis, ouvre le Store via `ksp-store-lib`, exige un health `Ready` puis lance le Worker exact via `RawTransactionIngestWorker::start_with_runtime_resources`. Les ressources physiques et handles restent exclusivement côté Rust.
|
||||
|
||||
Le runtime accepte désormais plusieurs routes simultanées sur un même réseau. Chaque route possède son Worker indépendant ; deux Starts de la même identité logique `profile_id + route_id` sont refusés, et un Start d'un autre réseau est refusé tant que le Store partagé reste ouvert. Le premier Worker ouvre et valide le Store, les suivants réutilisent le même `Arc<Store>`, puis la dernière route terminale déclenche seule la fermeture explicite du Store. Un fault d'une route ne stoppe pas les Workers siblings. Stop est ciblé par route et reste idempotent lorsqu'un terminal sûr a déjà été retenu.
|
||||
|
||||
Les races de contrôle sont fermées côté backend : un Stop qui rencontre une route encore `Starting` marque cette réservation pour arrêt coopératif et attend son cleanup au lieu de la considérer inactive. Dès qu'une fermeture de l'application commence, tout nouveau Start est refusé, les routes actives reçoivent Stop et les Starts déjà réservés sont marqués avant activation. La fenêtre principale ne quitte le processus qu'après cleanup borné des Workers et du Store partagé, ou avec un code d'échec après expiration de la borne.
|
||||
|
||||
Les profils applicatifs sont réseau-centriques (`devnet`, `mainnet`, `testnet`). Un profil peut agréger plusieurs profils Transport du même réseau : le provider reste une source de capability et ne devient jamais une identité réseau ni un choix de profil applicatif.
|
||||
Une route explicitement liée à un provider n’est projetée pour un réseau que si le composite déclare une source Transport de ce provider. Ainsi, la route Helius Transaction existe sur Devnet/Mainnet mais n’est pas présentée sur Testnet tant qu’Helius ne fournit pas de source Testnet configurée.
|
||||
|
||||
Le frontend ne reçoit aucune URL, credential, URI Store, metadata secrète, source key, handle runtime ou payload `RawTransaction`.
|
||||
|
||||
Les requêtes IPC Start/Stop sont shape-strict : les champs inconnus sont rejetés et `profile_id` est borné/validé avant toute reconstruction. Le bridge de logging frontend applique également des bornes strictes et refuse les caractères de contrôle avant émission dans la façade Logging.
|
||||
|
||||
## Routes V1
|
||||
|
||||
```text
|
||||
yellowstone-hydrated
|
||||
standard-logs-hydrated
|
||||
standard-block-direct
|
||||
helius-transaction-hydrated
|
||||
http-block-polling
|
||||
```
|
||||
|
||||
Les profils standard publics committés déclarent `Block` en plus de `Logs`; `standard-block-direct` est donc composable depuis Config sur Devnet, Mainnet et Testnet. Cette déclaration ne transforme pas `blockSubscribe` en méthode stable et ne remplace pas la revalidation runtime.
|
||||
|
||||
Une route `Configured` est uniquement **composable depuis Config**. La disponibilité réelle du Store et du Worker est revalidée lors du Start avant publication de l'accusé runtime. Le backend relaie désormais chaque Worker actif par un snapshot latest-value sûr : lifecycle, health, activity, admission/persistence, backpressure, reconnect/replay, continuité, gaps et repair. Les mises à jour sont coalescées côté backend à une cadence bornée de `500 ms` maximum par route, émises via l'événement Tauri `ksp-raw-ingest-route-status` et peuvent être resynchronisées avec `get_route_monitoring`. Les compteurs et slots `u64` sont projetés en texte décimal afin de rester exacts côté JavaScript. Le frontend consomme maintenant ce flux en temps réel et via resynchronisation explicite : chaque route affiche lifecycle/health/activity, persisted/source/gap summaries et ouvre un détail complet pipeline/persistence, reconnect/replay, continuité, gaps et repair. Les événements monitoring mettent aussi à jour les états actifs/terminaux afin que le sélecteur réseau et le Store partagé suivent le lifecycle réellement publié par le Worker. Les snapshots steady-state patchent les champs de carte en place ; la reconstruction des contrôles Start/Stop est réservée aux changements de lifecycle, afin de préserver la réactivité des interactions.
|
||||
|
||||
Pour `yellowstone-hydrated`, la stratégie Mainnet n'effectue plus un `getTransaction` par notification transactionnelle. Le Desk construit un abonnement Yellowstone Block léger ; chaque bloc observé déclenche une reconciliation HTTP `getBlock` Full/Base64 sur le même réseau. Le profil `publicnode_mainnet` expose un pool HTTP logique `default` contenant PublicNode et le RPC public Solana comme fallback de même priorité. Le pool conserve les limites propres à chaque endpoint, son health/cooldown et son round-robin interne. La reconciliation Yellowstone tolère un décalage bref entre le gRPC et les RPC HTTP grâce à un retry borné et interruptible par Stop. Les slots gRPC et hydrations `getBlock` restent bornés ; lorsqu'un consumer est saturé, la queue Transport attend de la capacité et propage la backpressure au stream au lieu de produire un `grpc_backpressure_overflow` local. Après un Stop explicite, un status provider reçu pendant le half-close ne remplace plus la fermeture coopérative ; un status observé pendant une session active reste soumis au reconnect/failure normal. Le chemin protobuf -> RAW direct reste différé tant que la canonicalisation exacte du meta n'est pas prouvée pour toutes les formes supportées.
|
||||
|
||||
La convergence Store de `0.3.15` conserve également une exception volontairement étroite : si un canonique complet existe déjà et qu'une route apporte le même RAW hors `logMessages` avec un marqueur exact `Log truncated` après un préfixe identique, l'observation moins complète est acceptée sans remplacer le canonique ni fault la route. Le sens inverse et toute divergence non prouvée restent des conflits terminaux dans cette version ; la gestion durable de variantes/résolutions appartient à `0.3.16`.
|
||||
|
||||
## Ports de développement
|
||||
|
||||
```text
|
||||
Vite HTTP : 1440
|
||||
Vite WS : 1441
|
||||
```
|
||||
|
||||
Lancement :
|
||||
|
||||
```bash
|
||||
(cd crates/ksp-app-raw-transaction-ingest-desk && cargo tauri dev)
|
||||
```
|
||||
|
||||
Le cycle frontend est déclenché par Tauri via ses hooks configurés ; les scripts npm applicatifs ne sont pas lancés directement par l'opérateur.
|
||||
|
||||
Voir [`USAGE.md`](USAGE.md) et [`../../docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md`](../../docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md).
|
||||
84
crates/ksp-app-raw-transaction-ingest-desk/USAGE.md
Normal file
84
crates/ksp-app-raw-transaction-ingest-desk/USAGE.md
Normal file
@@ -0,0 +1,84 @@
|
||||
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/USAGE.md -->
|
||||
<!-- version: 14 -->
|
||||
|
||||
# Utilisation de `ksp-app-raw-transaction-ingest-desk`
|
||||
|
||||
## 1. Lancement
|
||||
|
||||
Depuis la racine du workspace :
|
||||
|
||||
```bash
|
||||
(cd crates/ksp-app-raw-transaction-ingest-desk && cargo tauri dev)
|
||||
```
|
||||
|
||||
Tauri déclenche lui-même les hooks frontend configurés. Les scripts npm applicatifs de développement, contrôle ou build ne sont pas lancés directement par l'opérateur.
|
||||
|
||||
Le runtime prépare les ressources Config nécessaires, charge le composite dédié, initialise Logging puis ouvre le shell desktop.
|
||||
|
||||
## 2. Vue Routes
|
||||
|
||||
La vue Routes charge un inventaire déterministe construit côté Rust depuis le composite Config validé. Le sélecteur présente les réseaux logiques `devnet`, `mainnet` et `testnet`, jamais les providers. Chaque réseau agrège les profils Transport same-network configurés pour ses capabilities. La vue projette, pour chaque route V1 :
|
||||
|
||||
```text
|
||||
route_id
|
||||
famille et label sûrs
|
||||
réseau logique lorsque résolu
|
||||
état Config
|
||||
selectable oui/non
|
||||
raison sûre lorsque la route est indisponible
|
||||
génération d'inventaire
|
||||
```
|
||||
|
||||
`Configured` signifie uniquement que toutes les requirements Config nécessaires à la route sont cohérentes. Les profils `solana-public` standard committés déclarent explicitement les capabilities `Logs` et `Block`; `standard-block-direct` est donc Configured sur les trois réseaux logiques tant que ce socle Config reste valide. L'inventaire seul ne fait aucun probe réseau et n'ouvre aucun Store ni Worker.
|
||||
|
||||
Un refresh reconstruit l'inventaire depuis Config et publie une nouvelle génération. La sélection visuelle d'un réseau ne choisit aucune URL, provider ou ressource physique et ne démarre aucune acquisition. Une capability optionnelle dont le secret n'est pas résolu rend uniquement la route qui en dépend indisponible ; les routes satisfaites par le Transport de base du même réseau restent composables.
|
||||
Une route provider-specific dont aucune source n’est déclarée pour le réseau n’est pas une erreur de configuration : elle est hors inventaire pour ce réseau. Par exemple, Helius Transaction est projetée sur Devnet/Mainnet lorsque `transport.helius` existe, mais pas sur Testnet lorsqu’aucune source Helius Testnet n’est déclarée.
|
||||
|
||||
Les URLs, tokens, metadata secrètes, handles Transport/Store/Worker, URI Store, source keys et payloads RAW restent backend-only.
|
||||
|
||||
Les commandes Start/Stop refusent les champs IPC inconnus et les identifiants logiques non bornés ou contenant des caractères de contrôle. Le logging frontend est lui aussi borné avant l'IPC ; une entrée hostile ne doit jamais être recopiée dans les diagnostics backend.
|
||||
|
||||
### Start/Stop multi-route
|
||||
|
||||
Pour une route `Configured`, choisir `confirmed` ou `finalized` puis utiliser `Start`. Le backend revalide la génération d'inventaire et la composition Config, ouvre le Store correspondant lorsqu'aucun Store compatible n'est encore actif, exige son état `Ready`, construit le Worker exact puis installe son handle avant de renvoyer l'accusé runtime.
|
||||
|
||||
Plusieurs routes peuvent être actives simultanément lorsqu'elles appartiennent au même réseau logique. Chaque route conserve son Worker indépendant et son Stop ciblé. Le Store est partagé entre ces Workers ; le dernier Worker terminal déclenche la fermeture explicite du Store. Une route `Faulted` ne force pas l'arrêt des autres routes. Un Start d'un autre réseau est refusé tant que le Store partagé du réseau courant reste ouvert.
|
||||
|
||||
Un Stop peut être demandé même si le Start ciblé n'a pas encore fini d'ouvrir le Store ou d'installer le Worker : la réservation est alors marquée pour arrêt et le backend attend sa résolution. Lors de la fermeture de la fenêtre principale, le Desk ferme d'abord l'admission Start, demande l'arrêt de toutes les routes actives ou encore en démarrage et attend un cleanup borné avant de quitter. Il n'est donc pas nécessaire d'arrêter manuellement chaque route avant de fermer l'application.
|
||||
|
||||
Le backend expose désormais un flux latest-value par route via l'événement Tauri `ksp-raw-ingest-route-status`. Chaque projection contient uniquement l'identité logique sûre de la route, lifecycle/health/activity, compteurs admission/persistence, backpressure, reconnect/replay, continuité, gaps et repair. La commande `get_route_monitoring` permet une resynchronisation explicite des routes actives et des derniers terminaux retenus dans la session runtime courante. Les séquences, slots et compteurs `u64` sont transmis en texte décimal pour préserver leur exactitude côté JavaScript. Le bouton `Resync monitoring` recharge explicitement les latest values backend ; les événements `ksp-raw-ingest-route-status` actualisent ensuite les cartes sans polling frontend. Le relais backend coalesce les changements à une cadence maximale d'une émission toutes les `500 ms` par route, et les changements steady-state mettent à jour la carte en place sans recréer les boutons Start/Stop. Une carte disposant d'un snapshot montre un résumé sûr (`health`, `activity`, persisted, sources actives, gaps) et le bouton `Supervision` ouvre le détail pipeline/persistence, sources, reconnect/replay, continuité et repair. Les derniers terminaux de la session restent consultables jusqu'à remplacement par un nouveau Start ou changement de session réseau.
|
||||
|
||||
Sur Mainnet, `yellowstone-hydrated` utilise un abonnement Yellowstone Block comme signal de temps réel puis reconcile chaque slot via `getBlock` Full/Base64. Cette stratégie évite l'hydration `getTransaction` unitaire par transaction. Le profil PublicNode peut exposer plusieurs endpoints HTTP sous le même rôle logique ; Transport distribue alors les requêtes selon ses règles de priorité, disponibilité, concurrence et cooldown. La livraison gRPC et les hydrations restent bornées : une saturation locale applique de la backpressure au stream au lieu de dropper les updates ou de fault sur `grpc_backpressure_overflow`. Lors d'un Stop ciblé, un status distant reçu après le half-close local est une fermeture coopérative ; une erreur gRPC observée avant Stop reste une panne source/reconnect normale. La conversion directe du payload Yellowstone vers le RAW canonique reste réservée à une évolution ultérieure tant que la parité complète du meta n'est pas prouvée.
|
||||
|
||||
Lorsque plusieurs routes observent la même transaction, l'entité canonique reste unique et les provenances restent séparées. Le cas `canonique complet + entrant explicitement Log truncated compatible` est accepté sans remplacer le canonique ni passer la route en `Faulted`. Les autres divergences de contenu restent fail-closed en `0.3.15`; les variantes conflictuelles durables et leur résolution via Store Desk sont prévues pour `0.3.16`.
|
||||
|
||||
## 3. Raisons d'indisponibilité
|
||||
|
||||
Les reasons exposées sont des codes applicatifs bornés, par exemple :
|
||||
|
||||
```text
|
||||
profile_unresolved
|
||||
network_mismatch
|
||||
missing_http_get_block
|
||||
missing_http_get_transaction
|
||||
missing_http_block_scan
|
||||
missing_ws_logs_capability
|
||||
missing_ws_block_capability
|
||||
missing_helius_transaction_capability
|
||||
missing_yellowstone_grpc
|
||||
missing_required_secret
|
||||
```
|
||||
|
||||
Le texte arbitraire d'une erreur provider ou d'un secret Config n'est jamais projeté dans l'inventaire.
|
||||
|
||||
## 4. Diagnostics
|
||||
|
||||
La vue Diagnostics expose uniquement des informations techniques sûres nécessaires à l'exploitation du Desk, notamment l'identité de version, l'état du shell, la composition Config bootstrap, l'état Logging et la génération d'inventaire.
|
||||
|
||||
Les diagnostics ne doivent pas exposer de credentials, d'URI Store, d'URL d'endpoint, de source key, de handle runtime ou de payload `RawTransaction`.
|
||||
|
||||
## 5. Runtime packagé
|
||||
|
||||
Le build Tauri embarque les documents Config et schemas enregistrés. `ksp-config-lib` prépare ensuite la racine KSP user-writable sans embarquer `.env`.
|
||||
|
||||
Les fichiers Config et les secrets restent administrés par les mécanismes Config KSP ; l'interface ne contourne pas cette ownership.
|
||||
12
crates/ksp-app-raw-transaction-ingest-desk/build.rs
Normal file
12
crates/ksp-app-raw-transaction-ingest-desk/build.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/build.rs
|
||||
// version: 1
|
||||
|
||||
//! Build script for the KSP Raw Transaction Ingest desktop application.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Default capability for KSP Raw Transaction Ingest Desk",
|
||||
"windows": [
|
||||
"splash",
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"tracing:default"
|
||||
]
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/frontend/fonts/README.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Fonts
|
||||
|
||||
`DOS_Amazigh.ttf` est repris byte-exact du gabarit Desk KSP existant pour préserver l'identité visuelle commune.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 611 KiB |
231
crates/ksp-app-raw-transaction-ingest-desk/frontend/main.html
Normal file
231
crates/ksp-app-raw-transaction-ingest-desk/frontend/main.html
Normal file
@@ -0,0 +1,231 @@
|
||||
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/frontend/main.html -->
|
||||
<!-- version: 7 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<link rel="icon" href="imgs/logo.png">
|
||||
<link rel="stylesheet" href="sass/main.scss">
|
||||
<title>Raw Transaction Ingest Desk — Routes</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="app-header">
|
||||
<nav class="navbar h-100 py-0 bg-light text-dark">
|
||||
<div class="container-fluid px-4 flex-nowrap">
|
||||
<div class="navbar-brand d-flex align-items-center me-4 flex-nowrap">
|
||||
<img alt="KSP" src="imgs/logo.png" class="app-logo">
|
||||
<span class="ps-2 fs-4 fw-semibold text-primary text-nowrap">Raw Transaction Ingest Desk</span>
|
||||
<span class="mx-2 fs-5 text-body-secondary" aria-hidden="true">—</span>
|
||||
<span id="headerViewTitle" class="fs-5 text-body text-nowrap">Routes</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 ms-auto">
|
||||
<span id="headerProfile" class="badge text-bg-light border text-dark">—</span>
|
||||
<span id="headerStore" class="badge text-bg-secondary">Store idle</span>
|
||||
<span id="headerPhase" class="badge text-bg-secondary">inventory</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="app-main container-fluid p-0">
|
||||
<div class="row g-0 h-100 flex-nowrap">
|
||||
<aside class="col-auto app-sidebar border-end bg-body-tertiary">
|
||||
<div class="app-sidebar-scroll h-100" data-simplebar>
|
||||
<nav class="nav nav-pills flex-column gap-1 p-3" aria-label="Navigation Raw Transaction Ingest Desk">
|
||||
<button class="nav-link text-start active" type="button" data-view="routes">
|
||||
<i class="fa-solid fa-route me-2" aria-hidden="true"></i>Routes
|
||||
</button>
|
||||
<button class="nav-link text-start" type="button" data-view="diagnostics">
|
||||
<i class="fa-solid fa-stethoscope me-2" aria-hidden="true"></i>Diagnostics
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
<section class="col app-content app-scrollable h-100" data-simplebar>
|
||||
<div class="container-fluid px-4 py-4">
|
||||
<div class="card app-shell-card mx-auto shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<section data-view-panel="routes">
|
||||
<div class="d-flex align-items-start justify-content-between gap-3 flex-wrap mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">Routes live</h1>
|
||||
<p class="text-body-secondary mb-0">Inventaire Config et runtime multi-route avec un Worker indépendant par route et Store partagé same-network.</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<button id="refreshRouteMonitoring" class="btn btn-outline-secondary btn-sm" type="button">
|
||||
<i class="fa-solid fa-wave-square me-2" aria-hidden="true"></i>Resync monitoring
|
||||
</button>
|
||||
<button id="refreshRouteInventory" class="btn btn-outline-primary btn-sm" type="button">
|
||||
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh inventory
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 align-items-end mb-4">
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label" for="routeProfile">Réseau logique</label>
|
||||
<select id="routeProfile" class="form-select" aria-label="Réseau Raw Transaction Ingest Desk"></select>
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<label class="form-label" for="routeCommitment">Commitment</label>
|
||||
<select id="routeCommitment" class="form-select" aria-label="Commitment Raw Transaction Ingest Desk">
|
||||
<option value="confirmed" selected>confirmed</option>
|
||||
<option value="finalized">finalized</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="small text-body-secondary">Réseau</div>
|
||||
<div id="routeNetwork" class="fw-semibold app-route-code">—</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="small text-body-secondary">Génération</div>
|
||||
<div id="routeInventoryGeneration" class="fw-semibold app-route-code">—</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-info" role="status">
|
||||
<strong>Configured</strong> signifie que Config prouve la composabilité. Start revalide Config puis lance un Worker indépendant. Les routes du même réseau partagent le Store ; chaque fault reste isolé à sa route.
|
||||
</div>
|
||||
<div id="routeCommandFeedback" class="alert alert-info" role="status" aria-live="polite" hidden></div>
|
||||
<div class="card border-primary-subtle mb-4">
|
||||
<div class="card-body d-flex align-items-center justify-content-between gap-3 flex-wrap">
|
||||
<div>
|
||||
<div class="small text-body-secondary">Runtime multi-route / Store partagé</div>
|
||||
<div class="d-flex flex-wrap gap-2 mt-1">
|
||||
<span id="routeRuntimeState" class="badge text-bg-secondary">idle</span>
|
||||
<span id="routeRuntimeIdentity" class="app-route-code small text-body-secondary">aucun Worker actif</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small text-body-secondary mb-3">Réseau affiché : <span id="routeSelectedProfile" class="app-route-code">—</span></div>
|
||||
<div id="routeFoundation" class="row g-3 app-route-grid" aria-live="polite"></div>
|
||||
|
||||
<section id="routeMonitoringDetail" class="card shadow-sm mt-4 app-monitoring-detail" aria-live="polite" hidden>
|
||||
<div class="card-header d-flex align-items-center justify-content-between gap-3 flex-wrap">
|
||||
<div>
|
||||
<div class="fw-semibold">Supervision route</div>
|
||||
<div id="monitorRouteId" class="small text-body-secondary app-route-code">—</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-wrap align-items-center">
|
||||
<span id="monitorLifecycle" class="badge text-bg-secondary">—</span>
|
||||
<span id="monitorHealthBadge" class="badge text-bg-secondary">—</span>
|
||||
<span id="monitorActivityBadge" class="badge text-bg-light border text-dark">—</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-6 col-lg-3"><div class="small text-body-secondary">Network</div><div id="monitorNetwork" class="app-route-code fw-semibold">—</div></div>
|
||||
<div class="col-6 col-lg-3"><div class="small text-body-secondary">Commitment</div><div id="monitorCommitment" class="app-route-code fw-semibold">—</div></div>
|
||||
<div class="col-6 col-lg-3"><div class="small text-body-secondary">Sequence</div><div id="monitorSequence" class="app-route-code fw-semibold">—</div></div>
|
||||
<div class="col-6 col-lg-3"><div class="small text-body-secondary">Fault</div><div id="monitorFault" class="app-route-code fw-semibold">—</div></div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="card h-100 border-light-subtle">
|
||||
<div class="card-header fw-semibold">Pipeline / persistence</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0 app-monitoring-list">
|
||||
<dt class="col-7">Health</dt><dd id="monitorHealth" class="col-5">—</dd>
|
||||
<dt class="col-7">Activity</dt><dd id="monitorActivity" class="col-5">—</dd>
|
||||
<dt class="col-7">Admission queue</dt><dd id="monitorAdmissionQueue" class="col-5">—</dd>
|
||||
<dt class="col-7">Persistence in-flight</dt><dd id="monitorPersistence" class="col-5">—</dd>
|
||||
<dt class="col-7">Admitted</dt><dd id="monitorAdmittedTotal" class="col-5">—</dd>
|
||||
<dt class="col-7">Canonicalized</dt><dd id="monitorCanonicalizedTotal" class="col-5">—</dd>
|
||||
<dt class="col-7">Persisted</dt><dd id="monitorPersistedTotal" class="col-5">—</dd>
|
||||
<dt class="col-7">RAW inserted</dt><dd id="monitorEntityInsertedTotal" class="col-5">—</dd>
|
||||
<dt class="col-7">RAW already present</dt><dd id="monitorEntityAlreadyPresentTotal" class="col-5">—</dd>
|
||||
<dt class="col-7">RAW purged skipped</dt><dd id="monitorEntitySkippedPurgedTotal" class="col-5">—</dd>
|
||||
<dt class="col-7">Observations inserted</dt><dd id="monitorObservationInsertedTotal" class="col-5">—</dd>
|
||||
<dt class="col-7">Observations already present</dt><dd id="monitorObservationAlreadyPresentTotal" class="col-5">—</dd>
|
||||
<dt class="col-7">Content conflicts</dt><dd id="monitorContentConflictTotal" class="col-5">—</dd>
|
||||
<dt class="col-7">Store failures</dt><dd id="monitorStoreFailureTotal" class="col-5">—</dd>
|
||||
<dt class="col-7">Source failures</dt><dd id="monitorSourceFailureTotal" class="col-5">—</dd>
|
||||
<dt class="col-7">Backpressure waits</dt><dd id="monitorBackpressureWaitTotal" class="col-5">—</dd>
|
||||
<dt class="col-7">Hydration pending</dt><dd id="monitorHydrationPending" class="col-5">—</dd>
|
||||
<dt class="col-7">Processing frontier</dt><dd id="monitorProcessingFrontier" class="col-5">—</dd>
|
||||
<dt class="col-7">Oldest pending slot</dt><dd id="monitorOldestPending" class="col-5">—</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="card h-100 border-light-subtle">
|
||||
<div class="card-header fw-semibold">Sources / continuité / repair</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0 app-monitoring-list">
|
||||
<dt class="col-7">Source state</dt><dd id="monitorSourceState" class="col-5">—</dd>
|
||||
<dt class="col-7">Source counts</dt><dd id="monitorSourceCounts" class="col-5">—</dd>
|
||||
<dt class="col-7">Reconnects</dt><dd id="monitorSourceReconnectTotal" class="col-5">—</dd>
|
||||
<dt class="col-7">Replay attempts</dt><dd id="monitorSourceReplayAttemptTotal" class="col-5">—</dd>
|
||||
<dt class="col-7">Continuity gaps source</dt><dd id="monitorSourceContinuityGapTotal" class="col-5">—</dd>
|
||||
<dt class="col-7">Policy observed</dt><dd id="monitorContinuityPolicyObserved" class="col-5">—</dd>
|
||||
<dt class="col-7">Continuity frontier</dt><dd id="monitorContinuityFrontier" class="col-5">—</dd>
|
||||
<dt class="col-7">Open gaps</dt><dd id="monitorContinuityHasOpenGaps" class="col-5">—</dd>
|
||||
<dt class="col-7">Failed losses reconciled</dt><dd id="monitorFailedSourceLossesReconciled" class="col-5">—</dd>
|
||||
<dt class="col-7">Future coverage</dt><dd id="monitorFutureTargetCoverage" class="col-5">—</dd>
|
||||
<dt class="col-7">Open / repairing</dt><dd class="col-5"><span id="monitorOpenGapCount">—</span> / <span id="monitorRepairingGapCount">—</span></dd>
|
||||
<dt class="col-7">Repaired / unresolved</dt><dd class="col-5"><span id="monitorRepairedGapTotal">—</span> / <span id="monitorUnresolvedGapTotal">—</span></dd>
|
||||
<dt class="col-7">Oldest open gap</dt><dd id="monitorOldestOpenGap" class="col-5">—</dd>
|
||||
<dt class="col-7">Replay repair</dt><dd id="monitorReplayRepairTotal" class="col-5">—</dd>
|
||||
<dt class="col-7">Redundant coverage repair</dt><dd id="monitorRedundantCoverageRepairTotal" class="col-5">—</dd>
|
||||
<dt class="col-7">HTTP scan repair</dt><dd id="monitorHttpScanRepairTotal" class="col-5">—</dd>
|
||||
<dt class="col-7">Block fetch repair</dt><dd id="monitorRepairBlockFetchTotal" class="col-5">—</dd>
|
||||
<dt class="col-7">Transaction hydration repair</dt><dd id="monitorRepairTransactionHydrationTotal" class="col-5">—</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<div class="fw-semibold mb-2">Gaps courants / récents</div>
|
||||
<div id="monitorGapList" class="list-group app-monitoring-gaps"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section data-view-panel="diagnostics" hidden>
|
||||
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">Diagnostics</h1>
|
||||
<p class="text-body-secondary mb-0">État sûr du shell Config, Logging et de la génération d'inventaire.</p>
|
||||
</div>
|
||||
<button id="refreshRuntimeStatus" class="btn btn-outline-primary btn-sm" type="button">
|
||||
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header fw-semibold">Runtime</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0 app-runtime-list">
|
||||
<dt class="col-sm-5">Version</dt><dd id="runtimeVersion" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Phase</dt><dd id="runtimeShellPhase" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Documents Config</dt><dd id="runtimeConfigDocuments" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Profil composite bootstrap</dt><dd id="runtimeCompositeProfile" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Profil Logging</dt><dd id="runtimeLoggingProfile" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Fallback Logging</dt><dd id="runtimeLoggingFallback" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Inventaire default</dt><dd id="runtimeInventoryDefaultProfile" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Génération inventaire</dt><dd id="runtimeInventoryGeneration" class="col-sm-7">—</dd>
|
||||
</dl>
|
||||
<div id="runtimeDiagnostic" class="alert alert-warning mt-3 mb-0" role="status" aria-live="polite" hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="app-footer bg-dark text-light">
|
||||
<div class="container h-100 d-flex align-items-center justify-content-center">
|
||||
<small>© 2026 SASEDEV</small>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="ts/main.ts" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,167 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/sass/_app.scss
|
||||
// version: 3
|
||||
|
||||
$app-header-height: 72px;
|
||||
$app-footer-height: 42px;
|
||||
$app-sidebar-width: 250px;
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: $gray-100;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
position: fixed;
|
||||
inset: 0 0 auto;
|
||||
height: $app-header-height;
|
||||
z-index: 1020;
|
||||
border-bottom: 2px solid rgba($primary, 0.3);
|
||||
}
|
||||
|
||||
.app-footer {
|
||||
position: fixed;
|
||||
inset: auto 0 0;
|
||||
height: $app-footer-height;
|
||||
z-index: 1020;
|
||||
border-top: 2px solid rgba($primary, 0.3);
|
||||
}
|
||||
|
||||
.app-main {
|
||||
position: relative;
|
||||
height: calc(100vh - $app-header-height - $app-footer-height);
|
||||
margin-top: $app-header-height;
|
||||
margin-bottom: $app-footer-height;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-main > .row {
|
||||
flex-wrap: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-logo {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.app-scrollable {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.app-shell-card {
|
||||
width: 100%;
|
||||
max-width: 1320px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-sidebar {
|
||||
width: $app-sidebar-width;
|
||||
min-width: $app-sidebar-width;
|
||||
max-width: $app-sidebar-width;
|
||||
}
|
||||
|
||||
.app-sidebar-scroll,
|
||||
.app-content {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-sidebar .nav-link {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-runtime-list dd,
|
||||
.app-route-code {
|
||||
font-family: var(--bs-font-monospace);
|
||||
}
|
||||
|
||||
.app-route-grid {
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.app-route-grid > * {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-route-card {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
border-left: 4px solid rgba($primary, 0.55);
|
||||
}
|
||||
|
||||
.app-route-code {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.app-route-monitoring-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.app-monitoring-detail {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-monitoring-list dt,
|
||||
.app-monitoring-list dd {
|
||||
margin-bottom: 0.45rem;
|
||||
}
|
||||
|
||||
.app-monitoring-list dd,
|
||||
.app-monitoring-gaps,
|
||||
.app-monitoring-detail .app-route-code {
|
||||
font-family: var(--bs-font-monospace);
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.app-monitoring-gaps .list-group-item {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-placeholder {
|
||||
min-height: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
padding: 2rem;
|
||||
border: 1px dashed var(--bs-border-color);
|
||||
border-radius: var(--bs-border-radius-lg);
|
||||
color: var(--bs-secondary-color);
|
||||
background: var(--bs-body-bg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.app-placeholder i {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.app-sidebar {
|
||||
width: 190px;
|
||||
min-width: 190px;
|
||||
max-width: 190px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/sass/_bootswatch.scss
|
||||
// version: 1
|
||||
|
||||
// Pulse 5.3.8
|
||||
// Bootswatch
|
||||
|
||||
|
||||
// Variables
|
||||
|
||||
// Buttons
|
||||
|
||||
.btn {
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:active:focus,
|
||||
&.active:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&-secondary {
|
||||
color: $gray-900;
|
||||
background-color: $white;
|
||||
border-color: #ccc;
|
||||
|
||||
&:hover {
|
||||
color: $gray-900;
|
||||
background-color: $gray-300;
|
||||
border-color: $gray-500;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: tint-color($gray-900, 5%);
|
||||
background-color: $white;
|
||||
border-color: tint-color(#ccc, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
&-warning {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&-primary:focus {
|
||||
box-shadow: 0 0 5px tint-color($primary, 10%);
|
||||
}
|
||||
|
||||
&-secondary:focus {
|
||||
box-shadow: 0 0 5px $gray-400;
|
||||
}
|
||||
|
||||
&-success:focus {
|
||||
box-shadow: 0 0 5px tint-color($success, 10%);
|
||||
}
|
||||
|
||||
&-info:focus {
|
||||
box-shadow: 0 0 5px tint-color($info, 10%);
|
||||
}
|
||||
|
||||
&-warning:focus {
|
||||
box-shadow: 0 0 5px tint-color($warning, 10%);
|
||||
}
|
||||
|
||||
&-danger:focus {
|
||||
box-shadow: 0 0 5px tint-color($danger, 10%);
|
||||
}
|
||||
|
||||
&.disabled:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Tables
|
||||
|
||||
.table .thead-dark th {
|
||||
background-color: $secondary;
|
||||
border-color: $table-border-color;
|
||||
}
|
||||
|
||||
.table-primary,
|
||||
.table-secondary,
|
||||
.table-success,
|
||||
.table-warning,
|
||||
.table-danger,
|
||||
.table-info,
|
||||
.table-light {
|
||||
--#{$prefix}table-color: #{$body-color};
|
||||
}
|
||||
|
||||
// Forms
|
||||
|
||||
.form-control:focus {
|
||||
box-shadow: 0 0 5px rgba(100, 65, 164, .4);
|
||||
}
|
||||
|
||||
// Navs
|
||||
|
||||
.nav-tabs {
|
||||
|
||||
.nav-link,
|
||||
.nav-link.active {
|
||||
border-width: 0 0 1px;
|
||||
}
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link.active,
|
||||
.nav-link.active:hover,
|
||||
.nav-link.active:focus {
|
||||
border-bottom: 1px solid $primary;
|
||||
}
|
||||
|
||||
.nav-item+.nav-item {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
&-item.active {
|
||||
color: $gray-700;
|
||||
}
|
||||
}
|
||||
|
||||
// Indicators
|
||||
|
||||
.badge {
|
||||
&.bg-light {
|
||||
color: $dark;
|
||||
}
|
||||
}
|
||||
|
||||
// Progress bars
|
||||
|
||||
.progress {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
// Containers
|
||||
|
||||
.list-group {
|
||||
&-item {
|
||||
color: rgba(255, 255, 255, .8);
|
||||
|
||||
&.active,
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: 700;
|
||||
|
||||
&:hover {
|
||||
background-color: $list-group-hover-bg;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled:hover {
|
||||
color: $list-group-disabled-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/sass/_fontawesome.scss
|
||||
// version: 1
|
||||
|
||||
//@use '@fortawesome/fontawesome-free/scss/variables' with (
|
||||
// // customizing $font-path - make sure it points to where your webfonts are stored in your project
|
||||
// $font-path: '../webfonts',
|
||||
//);
|
||||
@use '@fortawesome/fontawesome-free/scss/variables' with (
|
||||
// use fonts from @fortawesome/fontawesome-free
|
||||
$font-path: '@fortawesome/fontawesome-free/webfonts',
|
||||
);
|
||||
// load Font Awesome core
|
||||
@use '@fortawesome/fontawesome-free/scss/fontawesome';
|
||||
|
||||
// load and make available Font Awesome helpers (mixins, functions, and variables)
|
||||
@use '@fortawesome/fontawesome-free/scss/fa' as fa;
|
||||
@use '@fortawesome/fontawesome-free/scss/brands' as fa-brands;
|
||||
@use '@fortawesome/fontawesome-free/scss/regular' as fa-regular;
|
||||
@use '@fortawesome/fontawesome-free/scss/solid' as fa-solid;
|
||||
@@ -0,0 +1,248 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/sass/_simplebar.scss
|
||||
// version: 1
|
||||
|
||||
/* Rtl support */
|
||||
[data-simplebar] {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
align-content: flex-start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.simplebar-wrapper {
|
||||
overflow: hidden;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
max-width: inherit;
|
||||
max-height: inherit;
|
||||
}
|
||||
|
||||
.simplebar-mask {
|
||||
direction: inherit;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
// z-index: 0;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.simplebar-offset {
|
||||
direction: inherit !important;
|
||||
box-sizing: inherit !important;
|
||||
resize: none !important;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.simplebar-content-wrapper {
|
||||
direction: inherit;
|
||||
box-sizing: border-box !important;
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-hide-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
position: fixed;
|
||||
left: 0;
|
||||
visibility: hidden;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.simplebar-content {
|
||||
&:before {
|
||||
content: ' ';
|
||||
display: table;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: ' ';
|
||||
display: table;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-placeholder {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.simplebar-height-auto-observer-wrapper {
|
||||
box-sizing: inherit !important;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 1px;
|
||||
position: relative;
|
||||
float: left;
|
||||
max-height: 1px;
|
||||
overflow: hidden;
|
||||
// z-index: -1;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
flex-grow: inherit;
|
||||
flex-shrink: 0;
|
||||
flex-basis: 0;
|
||||
}
|
||||
|
||||
.simplebar-height-auto-observer {
|
||||
box-sizing: inherit;
|
||||
display: block;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 1000%;
|
||||
width: 1000%;
|
||||
min-height: 1px;
|
||||
min-width: 1px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
// z-index: -1;
|
||||
}
|
||||
|
||||
.simplebar-track {
|
||||
// z-index: 1;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-simplebar].simplebar-dragging {
|
||||
pointer-events: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
.simplebar-content {
|
||||
pointer-events: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.simplebar-track {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-scrollbar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-height: 10px;
|
||||
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
background: black;
|
||||
border-radius: 7px;
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s 0.5s linear;
|
||||
top: 2px;
|
||||
bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-scrollbar.simplebar-visible {
|
||||
&:before {
|
||||
opacity: 0.5;
|
||||
transition-delay: 0s;
|
||||
transition-duration: 0s;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-track.simplebar-vertical {
|
||||
top: 0;
|
||||
width: 11px;
|
||||
}
|
||||
|
||||
.simplebar-track.simplebar-horizontal {
|
||||
left: 0;
|
||||
height: 11px;
|
||||
|
||||
.simplebar-scrollbar {
|
||||
right: auto;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
min-height: 0;
|
||||
min-width: 10px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
[data-simplebar-direction='rtl'] {
|
||||
.simplebar-track.simplebar-vertical {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-dummy-scrollbar-size {
|
||||
direction: rtl;
|
||||
position: fixed;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
height: 500px;
|
||||
width: 500px;
|
||||
overflow-y: hidden;
|
||||
overflow-x: scroll;
|
||||
-ms-overflow-style: scrollbar !important;
|
||||
|
||||
>div {
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/sass/_variables.scss
|
||||
// version: 1
|
||||
|
||||
// Pulse 5.3.8
|
||||
// Bootswatch
|
||||
|
||||
$theme: "pulse" !default;
|
||||
|
||||
//
|
||||
// Color system
|
||||
//
|
||||
|
||||
$white: #fff !default;
|
||||
$gray-100: #fafafa !default;
|
||||
$gray-200: #f9f8fc !default;
|
||||
$gray-300: #ededed !default;
|
||||
$gray-400: #cbc8d0 !default;
|
||||
$gray-500: #adb5bd !default;
|
||||
$gray-600: #868e96 !default;
|
||||
$gray-700: #444 !default;
|
||||
$gray-800: #343a40 !default;
|
||||
$gray-900: #17141f !default;
|
||||
$black: #000 !default;
|
||||
|
||||
$blue: #007bff !default;
|
||||
$indigo: #6610f2 !default;
|
||||
$purple: #593196 !default;
|
||||
$pink: #e83e8c !default;
|
||||
$red: #fc3939 !default;
|
||||
$orange: #fd7e14 !default;
|
||||
$yellow: #efa31d !default;
|
||||
$green: #13b955 !default;
|
||||
$teal: #20c997 !default;
|
||||
$cyan: #009cdc !default;
|
||||
|
||||
$primary: $purple !default;
|
||||
$secondary: #a991d4 !default;
|
||||
$success: $green !default;
|
||||
$info: $cyan !default;
|
||||
$warning: $yellow !default;
|
||||
$danger: $red !default;
|
||||
$light: $gray-200 !default;
|
||||
$dark: $gray-900 !default;
|
||||
|
||||
$min-contrast-ratio: 2.1 !default;
|
||||
|
||||
// Options
|
||||
|
||||
$enable-rounded: false !default;
|
||||
|
||||
// Body
|
||||
|
||||
$body-color: $gray-700 !default;
|
||||
|
||||
// Links
|
||||
|
||||
$link-hover-color: $primary !default;
|
||||
|
||||
// Tables
|
||||
|
||||
$table-color: initial !default;
|
||||
|
||||
$table-border-color: rgba(0, 0, 0, .05) !default;
|
||||
|
||||
// Forms
|
||||
|
||||
$input-focus-border-color: $primary !default;
|
||||
|
||||
// Dropdowns
|
||||
|
||||
$dropdown-link-hover-color: $white !default;
|
||||
$dropdown-link-hover-bg: $primary !default;
|
||||
|
||||
// Navs
|
||||
|
||||
$nav-tabs-border-color: $gray-300 !default;
|
||||
$nav-tabs-link-hover-border-color: $primary !default;
|
||||
|
||||
// Navbar
|
||||
|
||||
$navbar-padding-y: 1.2rem !default;
|
||||
|
||||
// Progress bars
|
||||
|
||||
$progress-bg: $gray-300 !default;
|
||||
$progress-bar-bg: $primary !default;
|
||||
|
||||
// List group
|
||||
|
||||
$list-group-bg: $gray-900 !default;
|
||||
$list-group-border-color: transparent !default;
|
||||
$list-group-hover-bg: lighten($list-group-bg, 10%) !default;
|
||||
$list-group-active-color: $white !default;
|
||||
$list-group-active-bg: $list-group-bg !default;
|
||||
$list-group-disabled-color: lighten($list-group-bg, 30%) !default;
|
||||
@@ -0,0 +1,10 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/sass/main.scss
|
||||
// version: 1
|
||||
|
||||
@import "bootstrap/scss/functions";
|
||||
@import "variables";
|
||||
@import "fontawesome";
|
||||
@import "simplebar";
|
||||
@import "bootstrap/scss/bootstrap";
|
||||
@import "bootswatch";
|
||||
@import "app";
|
||||
@@ -0,0 +1,137 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/sass/splash.scss
|
||||
// version: 1
|
||||
|
||||
@font-face {
|
||||
font-family: "Dos Amazigh";
|
||||
src: url("../fonts/DOS_Amazigh.ttf") format("truetype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
#splash-container {
|
||||
position: relative;
|
||||
width: 960px;
|
||||
max-width: 100%;
|
||||
height: 637px;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
#splash-image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
object-fit: contain;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#app-name {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 2;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #fff;
|
||||
font-family: "Dos Amazigh", sans-serif;
|
||||
font-size: 76px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
text-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#debug-info {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
width: 100%;
|
||||
max-height: 30%;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
#messages-container {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
max-height: 30%;
|
||||
overflow-y: auto;
|
||||
padding: 10px 10px 36px;
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
#debug-info::-webkit-scrollbar,
|
||||
#messages-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.splash-message {
|
||||
margin-bottom: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
animation: splashMessageFadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.splash-message.info { background: rgba(33, 150, 243, 0.22); }
|
||||
.splash-message.warning { background: rgba(255, 152, 0, 0.22); }
|
||||
.splash-message.error { background: rgba(244, 67, 54, 0.22); }
|
||||
.splash-message.success { background: rgba(76, 175, 80, 0.22); }
|
||||
|
||||
@keyframes splashMessageFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/frontend/splash.html -->
|
||||
<!-- version: 1 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<link rel="icon" href="imgs/logo.png">
|
||||
<link rel="stylesheet" href="sass/splash.scss">
|
||||
<title>KSP Raw Transaction Ingest Desk — Chargement</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="splash-container" style="opacity: 0;">
|
||||
<img id="splash-image" src="imgs/splash.png" alt="Chargement de KSP Raw Transaction Ingest Desk">
|
||||
<div id="app-name">RAW Ingest</div>
|
||||
<div id="debug-info" aria-live="off" hidden></div>
|
||||
<div id="messages-container" aria-live="polite"></div>
|
||||
</div>
|
||||
<script type="module" src="ts/splash.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,116 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/frontend_log.ts
|
||||
// version: 2
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { FrontendLogPayloadDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/frontend_logging/FrontendLogPayloadDto.ts";
|
||||
|
||||
export type FrontendLogLevel = "trace" | "debug" | "info" | "warn" | "error";
|
||||
export type FrontendLogTargetId = "frontend" | "main" | "splash";
|
||||
|
||||
type ConsoleMethod = (...items: unknown[]) => void;
|
||||
|
||||
const FRONTEND_LOG_MESSAGE_MAX_CODE_UNITS = 1024;
|
||||
|
||||
const originalConsole = {
|
||||
trace: console.trace.bind(console),
|
||||
debug: console.debug.bind(console),
|
||||
log: console.log.bind(console),
|
||||
info: console.info.bind(console),
|
||||
warn: console.warn.bind(console),
|
||||
error: console.error.bind(console),
|
||||
};
|
||||
|
||||
function stringifyItem(item: unknown): string {
|
||||
if (item instanceof Error) {
|
||||
return item.stack ?? item.message;
|
||||
}
|
||||
if (typeof item === "string") {
|
||||
return item;
|
||||
}
|
||||
try {
|
||||
const serialized = JSON.stringify(item);
|
||||
return serialized ?? String(item);
|
||||
} catch {
|
||||
return String(item);
|
||||
}
|
||||
}
|
||||
|
||||
function formatMessage(items: unknown[]): string {
|
||||
return items.map(item => stringifyItem(item)).join(" ");
|
||||
}
|
||||
|
||||
function boundMessage(message: string): string {
|
||||
return message.length <= FRONTEND_LOG_MESSAGE_MAX_CODE_UNITS ? message : `${message.slice(0, FRONTEND_LOG_MESSAGE_MAX_CODE_UNITS)}…`;
|
||||
}
|
||||
|
||||
async function sendFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTargetId, message: string): Promise<void> {
|
||||
const payload: FrontendLogPayloadDto = {
|
||||
level,
|
||||
targetId,
|
||||
message: boundMessage(message),
|
||||
};
|
||||
await invoke("emit_frontend_log", { payload });
|
||||
}
|
||||
|
||||
function writeOriginalConsole(level: FrontendLogLevel, message: string): void {
|
||||
return level === "trace"
|
||||
? originalConsole.trace(message)
|
||||
: level === "debug"
|
||||
? originalConsole.debug(message)
|
||||
: level === "info"
|
||||
? originalConsole.info(message)
|
||||
: level === "warn"
|
||||
? originalConsole.warn(message)
|
||||
: originalConsole.error(message);
|
||||
}
|
||||
|
||||
export async function emitFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTargetId, message: string): Promise<void> {
|
||||
writeOriginalConsole(level, message);
|
||||
await sendFrontendLog(level, targetId, message);
|
||||
}
|
||||
|
||||
export function frontendTrace(targetId: FrontendLogTargetId, ...items: unknown[]): void {
|
||||
const message = formatMessage(items);
|
||||
originalConsole.trace(message);
|
||||
void sendFrontendLog("trace", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
|
||||
}
|
||||
|
||||
export function frontendDebug(targetId: FrontendLogTargetId, ...items: unknown[]): void {
|
||||
const message = formatMessage(items);
|
||||
originalConsole.debug(message);
|
||||
void sendFrontendLog("debug", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
|
||||
}
|
||||
|
||||
export function frontendInfo(targetId: FrontendLogTargetId, ...items: unknown[]): void {
|
||||
const message = formatMessage(items);
|
||||
originalConsole.info(message);
|
||||
void sendFrontendLog("info", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
|
||||
}
|
||||
|
||||
export function frontendWarn(targetId: FrontendLogTargetId, ...items: unknown[]): void {
|
||||
const message = formatMessage(items);
|
||||
originalConsole.warn(message);
|
||||
void sendFrontendLog("warn", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
|
||||
}
|
||||
|
||||
export function frontendError(targetId: FrontendLogTargetId, ...items: unknown[]): void {
|
||||
const message = formatMessage(items);
|
||||
originalConsole.error(message);
|
||||
void sendFrontendLog("error", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
|
||||
}
|
||||
|
||||
function buildConsoleBridge(level: FrontendLogLevel, targetId: FrontendLogTargetId, original: ConsoleMethod): ConsoleMethod {
|
||||
return (...items: unknown[]) => {
|
||||
original(...items);
|
||||
void sendFrontendLog(level, targetId, formatMessage(items)).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
|
||||
};
|
||||
}
|
||||
|
||||
export function installFrontendConsoleBridge(targetId: FrontendLogTargetId): void {
|
||||
console.trace = buildConsoleBridge("trace", targetId, originalConsole.trace);
|
||||
console.debug = buildConsoleBridge("debug", targetId, originalConsole.debug);
|
||||
console.log = buildConsoleBridge("info", targetId, originalConsole.log);
|
||||
console.info = buildConsoleBridge("info", targetId, originalConsole.info);
|
||||
console.warn = buildConsoleBridge("warn", targetId, originalConsole.warn);
|
||||
console.error = buildConsoleBridge("error", targetId, originalConsole.error);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/invoke.ts
|
||||
// version: 1
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { frontendDebug, frontendError, frontendTrace, type FrontendLogTargetId } from "./frontend_log";
|
||||
|
||||
export async function invokeKsp<T>(targetId: FrontendLogTargetId, command: string, args?: Record<string, unknown>): Promise<T> {
|
||||
frontendDebug(targetId, "Frontend IPC command requested", { command });
|
||||
try {
|
||||
const result = await invoke<T>(command, args);
|
||||
frontendTrace(targetId, "Frontend IPC command completed", { command });
|
||||
return result;
|
||||
} catch (caughtError) {
|
||||
frontendError(targetId, "Frontend IPC command failed", { command });
|
||||
throw caughtError;
|
||||
}
|
||||
}
|
||||
760
crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/main.ts
Normal file
760
crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/main.ts
Normal file
@@ -0,0 +1,760 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/main.ts
|
||||
// version: 8
|
||||
|
||||
import "bootstrap";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import "simplebar";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import type { ShellStatusDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_common/ShellStatusDto.ts";
|
||||
import type { RawIngestCommitment } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestCommitment.ts";
|
||||
import type { RawIngestRouteId } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteId.ts";
|
||||
import type { RawIngestRouteInventoryDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteInventoryDto.ts";
|
||||
import type { RawIngestRouteRuntimeDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteRuntimeDto.ts";
|
||||
import type { RawIngestRouteStartRequestDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStartRequestDto.ts";
|
||||
import type { RawIngestRouteStopRequestDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStopRequestDto.ts";
|
||||
import type { RawIngestRouteMonitoringDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/route_monitoring/RawIngestRouteMonitoringDto.ts";
|
||||
import { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
|
||||
import { invokeKsp } from "./invoke";
|
||||
|
||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||
installFrontendConsoleBridge("main");
|
||||
|
||||
type ViewId = "routes" | "diagnostics";
|
||||
|
||||
const viewTitles: Record<ViewId, string> = {
|
||||
routes: "Routes",
|
||||
diagnostics: "Diagnostics",
|
||||
};
|
||||
|
||||
const activeRuntimes = new Map<string, RawIngestRouteRuntimeDto>();
|
||||
const lastRuntimes = new Map<string, RawIngestRouteRuntimeDto>();
|
||||
const routeMonitoring = new Map<string, RawIngestRouteMonitoringDto>();
|
||||
const routeCards = new Map<string, HTMLElement>();
|
||||
let routeInventory: RawIngestRouteInventoryDto | null = null;
|
||||
let selectedProfileId: string | null = null;
|
||||
let selectedMonitoringKey: string | null = null;
|
||||
|
||||
function isViewId(value: string | undefined): value is ViewId {
|
||||
return value === "routes" || value === "diagnostics";
|
||||
}
|
||||
|
||||
function activateView(viewId: ViewId, source: "startup" | "user"): void {
|
||||
document.querySelectorAll<HTMLElement>("[data-view-panel]").forEach(panel => {
|
||||
panel.hidden = panel.dataset.viewPanel !== viewId;
|
||||
});
|
||||
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
|
||||
const active = button.dataset.view === viewId;
|
||||
button.classList.toggle("active", active);
|
||||
button.setAttribute("aria-current", active ? "page" : "false");
|
||||
});
|
||||
const header = document.querySelector<HTMLElement>("#headerViewTitle");
|
||||
if (header) {
|
||||
header.textContent = viewTitles[viewId];
|
||||
}
|
||||
document.title = `Raw Transaction Ingest Desk — ${viewTitles[viewId]}`;
|
||||
frontendTrace("main", "Raw Transaction Ingest Desk view DOM updated", { viewId, source });
|
||||
}
|
||||
|
||||
function bindFrontendInteractions(): void {
|
||||
document.addEventListener(
|
||||
"click",
|
||||
event => {
|
||||
const source = event.target;
|
||||
if (!(source instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
const control = source.closest<HTMLElement>('button, a, input, select, textarea, [role="button"], [role="tab"], [data-view]');
|
||||
if (!control) {
|
||||
return;
|
||||
}
|
||||
frontendTrace("main", "Raw Transaction Ingest Desk frontend control clicked", {
|
||||
controlId: control.id || null,
|
||||
role: control.getAttribute("role"),
|
||||
tagName: control.tagName.toLowerCase(),
|
||||
viewId: control.dataset.view ?? null,
|
||||
});
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
function bindNavigation(): void {
|
||||
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
const viewId = button.dataset.view;
|
||||
frontendDebug("main", "Raw Transaction Ingest Desk navigation tab clicked", { viewId: viewId ?? "unknown" });
|
||||
if (isViewId(viewId)) {
|
||||
activateView(viewId, "user");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function reasonLabel(reason: string | null): string {
|
||||
const labels: Record<string, string> = {
|
||||
profile_unresolved: "profil Config non résolu",
|
||||
network_mismatch: "réseau Transport/Store incohérent",
|
||||
missing_http_get_block: "HTTP getBlock indisponible",
|
||||
missing_http_get_transaction: "HTTP getTransaction indisponible",
|
||||
missing_http_block_scan: "HTTP block scan incomplet",
|
||||
missing_ws_logs_capability: "capability WS Logs absente",
|
||||
missing_ws_block_capability: "capability WS Block absente",
|
||||
missing_helius_transaction_capability: "capability Helius Transaction absente",
|
||||
missing_yellowstone_grpc: "Yellowstone gRPC absent",
|
||||
missing_required_secret: "secret Config requis non résolu",
|
||||
};
|
||||
if (reason === null) {
|
||||
return "composable depuis Config";
|
||||
}
|
||||
return labels[reason] ?? reason;
|
||||
}
|
||||
|
||||
function runtimeKey(profileId: string, routeId: RawIngestRouteId): string {
|
||||
return `${profileId}::${routeId}`;
|
||||
}
|
||||
|
||||
function lifecycleBadgeClass(state: string): string {
|
||||
if (state === "faulted") {
|
||||
return "badge text-bg-danger";
|
||||
}
|
||||
if (state === "running") {
|
||||
return "badge text-bg-success";
|
||||
}
|
||||
if (state === "starting" || state === "stopping") {
|
||||
return "badge text-bg-primary";
|
||||
}
|
||||
if (state === "configured") {
|
||||
return "badge text-bg-info";
|
||||
}
|
||||
return "badge text-bg-secondary";
|
||||
}
|
||||
|
||||
function yesNo(value: boolean): string {
|
||||
return value ? "oui" : "non";
|
||||
}
|
||||
|
||||
function optionalText(value: string | null): string {
|
||||
return value ?? "—";
|
||||
}
|
||||
|
||||
function safeCommandErrorText(caughtError: unknown, fallback: string): string {
|
||||
if (typeof caughtError === "object" && caughtError !== null) {
|
||||
const candidate = caughtError as { code?: unknown; domain?: unknown; message?: unknown };
|
||||
if (typeof candidate.domain === "string" && typeof candidate.code === "string" && typeof candidate.message === "string") {
|
||||
return `${candidate.domain}/${candidate.code}: ${candidate.message}`;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function renderRouteFeedback(message: string, level: "danger" | "info" | "success"): void {
|
||||
const feedback = document.querySelector<HTMLElement>("#routeCommandFeedback");
|
||||
if (!feedback) {
|
||||
return;
|
||||
}
|
||||
feedback.hidden = false;
|
||||
feedback.classList.remove("alert-danger", "alert-info", "alert-success");
|
||||
feedback.classList.add(`alert-${level}`);
|
||||
feedback.textContent = message;
|
||||
}
|
||||
|
||||
function clearRouteFeedback(): void {
|
||||
const feedback = document.querySelector<HTMLElement>("#routeCommandFeedback");
|
||||
if (!feedback) {
|
||||
return;
|
||||
}
|
||||
feedback.hidden = true;
|
||||
feedback.textContent = "";
|
||||
}
|
||||
|
||||
function monitoringRuntime(status: RawIngestRouteMonitoringDto): RawIngestRouteRuntimeDto {
|
||||
return {
|
||||
commitment: status.commitment,
|
||||
inventoryGeneration: status.inventoryGeneration,
|
||||
network: status.network,
|
||||
profileId: status.profileId,
|
||||
routeId: status.routeId,
|
||||
state: status.state,
|
||||
};
|
||||
}
|
||||
|
||||
function recordRouteMonitoring(status: RawIngestRouteMonitoringDto): void {
|
||||
const key = runtimeKey(status.profileId, status.routeId);
|
||||
const runtime = monitoringRuntime(status);
|
||||
routeMonitoring.set(key, status);
|
||||
lastRuntimes.set(key, runtime);
|
||||
if (runtimeIsActive(runtime)) {
|
||||
activeRuntimes.set(key, runtime);
|
||||
} else {
|
||||
activeRuntimes.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
function selectRouteMonitoring(profileId: string, routeId: RawIngestRouteId, family: string, source: "event" | "user"): void {
|
||||
const key = runtimeKey(profileId, routeId);
|
||||
selectedMonitoringKey = key;
|
||||
renderMonitoringDetail();
|
||||
frontendDebug("main", "Raw Transaction Ingest Desk route supervision selected", {
|
||||
family,
|
||||
routeId,
|
||||
selected: routeMonitoring.has(key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
function renderSelectedProfile(): void {
|
||||
if (routeInventory === null) {
|
||||
return;
|
||||
}
|
||||
const profile = routeInventory.profiles.find(candidate => candidate.profileId === selectedProfileId) ?? routeInventory.profiles[0];
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
selectedProfileId = profile.profileId;
|
||||
text("#routeNetwork", profile.network ?? "non résolu");
|
||||
text("#routeSelectedProfile", profile.profileId);
|
||||
text("#headerProfile", profile.profileId);
|
||||
const routeRoot = document.querySelector<HTMLElement>("#routeFoundation");
|
||||
if (routeRoot) {
|
||||
routeCards.clear();
|
||||
routeRoot.replaceChildren();
|
||||
for (const route of profile.routes) {
|
||||
const key = runtimeKey(profile.profileId, route.routeId);
|
||||
const runtime = activeRuntimes.get(key) ?? lastRuntimes.get(key) ?? null;
|
||||
const monitoring = routeMonitoring.get(key) ?? null;
|
||||
const projectedState = monitoring?.state ?? runtime?.state ?? route.state;
|
||||
const active = projectedState === "starting" || projectedState === "running" || projectedState === "stopping";
|
||||
const column = document.createElement("div");
|
||||
column.className = "col-12 col-xl-6 app-route-column";
|
||||
const card = document.createElement("div");
|
||||
card.className = "card h-100 shadow-sm app-route-card";
|
||||
const body = document.createElement("div");
|
||||
body.className = "card-body";
|
||||
const heading = document.createElement("div");
|
||||
heading.className = "d-flex align-items-start justify-content-between gap-3";
|
||||
const headingText = document.createElement("div");
|
||||
const title = document.createElement("h2");
|
||||
title.className = "h6 mb-1";
|
||||
title.textContent = route.label;
|
||||
const code = document.createElement("div");
|
||||
code.className = "app-route-code text-body-secondary small";
|
||||
code.textContent = route.routeId;
|
||||
headingText.append(title, code);
|
||||
const family = document.createElement("span");
|
||||
family.className = "badge text-bg-light border text-dark";
|
||||
family.textContent = route.family;
|
||||
heading.append(headingText, family);
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "d-flex flex-wrap gap-2 mt-3 app-route-meta";
|
||||
const state = document.createElement("span");
|
||||
state.className = lifecycleBadgeClass(projectedState);
|
||||
state.dataset.monitoringField = "state";
|
||||
state.textContent = projectedState;
|
||||
const network = document.createElement("span");
|
||||
network.className = "badge text-bg-light border text-dark";
|
||||
network.textContent = route.network ?? "network unresolved";
|
||||
const reason = document.createElement("span");
|
||||
reason.className = route.selectable ? "badge text-bg-light border text-dark" : "badge text-bg-warning";
|
||||
reason.textContent = reasonLabel(route.reason);
|
||||
meta.append(state, network, reason);
|
||||
body.append(heading, meta);
|
||||
if (monitoring) {
|
||||
const summary = document.createElement("div");
|
||||
summary.className = "app-route-monitoring-summary mt-3";
|
||||
const health = document.createElement("span");
|
||||
health.className = monitoring.health === "healthy" ? "badge text-bg-success" : "badge text-bg-warning";
|
||||
health.dataset.monitoringField = "health";
|
||||
health.textContent = `health: ${monitoring.health}`;
|
||||
const activity = document.createElement("span");
|
||||
activity.className = "badge text-bg-light border text-dark";
|
||||
activity.dataset.monitoringField = "activity";
|
||||
activity.textContent = `activity: ${monitoring.activity}`;
|
||||
const persisted = document.createElement("span");
|
||||
persisted.className = "badge text-bg-light border text-dark";
|
||||
persisted.dataset.monitoringField = "persisted";
|
||||
persisted.textContent = `persisted: ${monitoring.persistedTotal}`;
|
||||
const sources = document.createElement("span");
|
||||
sources.className = "badge text-bg-light border text-dark";
|
||||
sources.dataset.monitoringField = "sources";
|
||||
sources.textContent = `sources: ${monitoring.sourceActive}/${monitoring.sourceTotal}`;
|
||||
const gaps = document.createElement("span");
|
||||
gaps.className = monitoring.openGapCount === 0 ? "badge text-bg-light border text-dark" : "badge text-bg-warning";
|
||||
gaps.dataset.monitoringField = "gaps";
|
||||
gaps.textContent = `gaps: ${monitoring.openGapCount}`;
|
||||
summary.append(health, activity, persisted, sources, gaps);
|
||||
body.append(summary);
|
||||
}
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "d-flex flex-wrap gap-2 mt-3";
|
||||
if (route.selectable) {
|
||||
const start = document.createElement("button");
|
||||
start.className = "btn btn-primary btn-sm";
|
||||
start.type = "button";
|
||||
start.textContent = "Start";
|
||||
start.disabled = active;
|
||||
start.dataset.routeId = route.routeId;
|
||||
start.addEventListener("click", () => {
|
||||
selectedMonitoringKey = key;
|
||||
clearRouteFeedback();
|
||||
void startRoute(route.routeId).catch(caughtError => {
|
||||
renderRouteFeedback(safeCommandErrorText(caughtError, "Le démarrage de la route a été refusé."), "danger");
|
||||
frontendWarn("main", "Raw Transaction Ingest Desk route Start failed", { routeId: route.routeId });
|
||||
});
|
||||
});
|
||||
actions.append(start);
|
||||
if (active && runtime !== null) {
|
||||
const stop = document.createElement("button");
|
||||
stop.className = "btn btn-outline-danger btn-sm";
|
||||
stop.type = "button";
|
||||
stop.textContent = "Stop";
|
||||
stop.disabled = projectedState === "stopping";
|
||||
stop.dataset.routeId = route.routeId;
|
||||
stop.addEventListener("click", () => {
|
||||
clearRouteFeedback();
|
||||
void stopRoute(runtime).catch(caughtError => {
|
||||
renderRouteFeedback(safeCommandErrorText(caughtError, "L'arrêt de la route a été refusé."), "danger");
|
||||
frontendWarn("main", "Raw Transaction Ingest Desk route Stop failed", { routeId: route.routeId });
|
||||
});
|
||||
});
|
||||
actions.append(stop);
|
||||
}
|
||||
}
|
||||
if (monitoring) {
|
||||
const supervise = document.createElement("button");
|
||||
supervise.className = selectedMonitoringKey === key ? "btn btn-secondary btn-sm" : "btn btn-outline-secondary btn-sm";
|
||||
supervise.type = "button";
|
||||
supervise.textContent = "Supervision";
|
||||
supervise.dataset.routeId = route.routeId;
|
||||
supervise.addEventListener("click", () => selectRouteMonitoring(profile.profileId, route.routeId, route.family, "user"));
|
||||
actions.append(supervise);
|
||||
}
|
||||
if (actions.childElementCount > 0) {
|
||||
body.append(actions);
|
||||
}
|
||||
card.append(body);
|
||||
routeCards.set(key, card);
|
||||
column.append(card);
|
||||
routeRoot.append(column);
|
||||
}
|
||||
}
|
||||
renderMonitoringDetail();
|
||||
frontendTrace("main", "Raw Transaction Ingest Desk Config route inventory profile rendered", {
|
||||
activeRouteCount: activeRuntimes.size,
|
||||
monitoringCount: routeMonitoring.size,
|
||||
profileId: profile.profileId,
|
||||
routeCount: profile.routes.length,
|
||||
selectableCount: profile.routes.filter(route => route.selectable).length,
|
||||
});
|
||||
}
|
||||
|
||||
function renderMonitoringDetail(): void {
|
||||
const panel = document.querySelector<HTMLElement>("#routeMonitoringDetail");
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
const monitoring = selectedMonitoringKey === null ? null : (routeMonitoring.get(selectedMonitoringKey) ?? null);
|
||||
if (!monitoring) {
|
||||
panel.hidden = true;
|
||||
return;
|
||||
}
|
||||
panel.hidden = false;
|
||||
text("#monitorRouteId", monitoring.routeId);
|
||||
text("#monitorNetwork", monitoring.network);
|
||||
text("#monitorCommitment", monitoring.commitment);
|
||||
text("#monitorSequence", monitoring.sequence);
|
||||
text("#monitorHealth", monitoring.health);
|
||||
text("#monitorActivity", monitoring.activity);
|
||||
text("#monitorSourceState", optionalText(monitoring.sourceState));
|
||||
text("#monitorFault", monitoring.faultDomain === null || monitoring.faultCode === null ? "—" : `${monitoring.faultDomain}/${monitoring.faultCode}`);
|
||||
const lifecycle = document.querySelector<HTMLElement>("#monitorLifecycle");
|
||||
if (lifecycle) {
|
||||
lifecycle.className = lifecycleBadgeClass(monitoring.state);
|
||||
lifecycle.textContent = monitoring.state;
|
||||
}
|
||||
const health = document.querySelector<HTMLElement>("#monitorHealthBadge");
|
||||
if (health) {
|
||||
health.className = monitoring.health === "healthy" ? "badge text-bg-success" : "badge text-bg-warning";
|
||||
health.textContent = monitoring.health;
|
||||
}
|
||||
const activity = document.querySelector<HTMLElement>("#monitorActivityBadge");
|
||||
if (activity) {
|
||||
activity.className = "badge text-bg-light border text-dark";
|
||||
activity.textContent = monitoring.activity;
|
||||
}
|
||||
const values: Record<string, string> = {
|
||||
monitorAdmissionQueue: `${monitoring.admissionQueueDepth} / ${monitoring.admissionQueueCapacity}`,
|
||||
monitorPersistence: `${monitoring.inFlightPersistence} / ${monitoring.persistenceConcurrency}`,
|
||||
monitorAdmittedTotal: monitoring.admittedTotal,
|
||||
monitorCanonicalizedTotal: monitoring.canonicalizedTotal,
|
||||
monitorPersistedTotal: monitoring.persistedTotal,
|
||||
monitorEntityInsertedTotal: monitoring.entityInsertedTotal,
|
||||
monitorEntityAlreadyPresentTotal: monitoring.entityAlreadyPresentTotal,
|
||||
monitorEntitySkippedPurgedTotal: monitoring.entitySkippedPurgedTotal,
|
||||
monitorObservationInsertedTotal: monitoring.observationInsertedTotal,
|
||||
monitorObservationAlreadyPresentTotal: monitoring.observationAlreadyPresentTotal,
|
||||
monitorContentConflictTotal: monitoring.contentConflictTotal,
|
||||
monitorStoreFailureTotal: monitoring.storeFailureTotal,
|
||||
monitorSourceFailureTotal: monitoring.sourceFailureTotal,
|
||||
monitorBackpressureWaitTotal: monitoring.backpressureWaitTotal,
|
||||
monitorHydrationPending: monitoring.hydrationPending.toString(),
|
||||
monitorProcessingFrontier: optionalText(monitoring.processingFrontierSlot),
|
||||
monitorOldestPending: optionalText(monitoring.oldestPendingSlot),
|
||||
monitorSourceCounts: `${monitoring.sourceActive} active / ${monitoring.sourceReconnecting} reconnecting / ${monitoring.sourceFailed} failed / ${monitoring.sourceTotal} total`,
|
||||
monitorSourceReconnectTotal: monitoring.sourceReconnectTotal,
|
||||
monitorSourceReplayAttemptTotal: monitoring.sourceReplayAttemptTotal,
|
||||
monitorSourceContinuityGapTotal: monitoring.sourceContinuityGapTotal,
|
||||
monitorContinuityPolicyObserved: yesNo(monitoring.continuityPolicyObserved),
|
||||
monitorContinuityFrontier: optionalText(monitoring.continuityFrontierSlot),
|
||||
monitorContinuityHasOpenGaps: yesNo(monitoring.continuityHasOpenGaps),
|
||||
monitorFailedSourceLossesReconciled: yesNo(monitoring.failedSourceLossesReconciled),
|
||||
monitorFutureTargetCoverage: yesNo(monitoring.futureTargetCoverage),
|
||||
monitorOpenGapCount: monitoring.openGapCount.toString(),
|
||||
monitorRepairingGapCount: monitoring.repairingGapCount.toString(),
|
||||
monitorRepairedGapTotal: monitoring.repairedGapTotal,
|
||||
monitorUnresolvedGapTotal: monitoring.unresolvedGapTotal,
|
||||
monitorOldestOpenGap: optionalText(monitoring.oldestOpenGapStartSlot),
|
||||
monitorReplayRepairTotal: monitoring.replayRepairTotal,
|
||||
monitorRedundantCoverageRepairTotal: monitoring.redundantCoverageRepairTotal,
|
||||
monitorHttpScanRepairTotal: monitoring.httpScanRepairTotal,
|
||||
monitorRepairBlockFetchTotal: monitoring.repairBlockFetchTotal,
|
||||
monitorRepairTransactionHydrationTotal: monitoring.repairTransactionHydrationTotal,
|
||||
};
|
||||
for (const [id, value] of Object.entries(values)) {
|
||||
text(`#${id}`, value);
|
||||
}
|
||||
const gaps = document.querySelector<HTMLElement>("#monitorGapList");
|
||||
if (gaps) {
|
||||
gaps.replaceChildren();
|
||||
if (monitoring.gaps.length === 0) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "text-body-secondary small";
|
||||
empty.textContent = "Aucun gap courant ou récent projeté.";
|
||||
gaps.append(empty);
|
||||
} else {
|
||||
for (const gap of monitoring.gaps) {
|
||||
const item = document.createElement("div");
|
||||
item.className = "list-group-item";
|
||||
const top = document.createElement("div");
|
||||
top.className = "d-flex justify-content-between gap-3 flex-wrap";
|
||||
const range = document.createElement("span");
|
||||
range.className = "app-route-code";
|
||||
range.textContent = `${gap.startSlot} – ${gap.endSlot}`;
|
||||
const badge = document.createElement("span");
|
||||
badge.className = gap.state === "open" ? "badge text-bg-warning" : "badge text-bg-secondary";
|
||||
badge.textContent = gap.state;
|
||||
top.append(range, badge);
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "small text-body-secondary mt-1";
|
||||
meta.textContent = `gap ${gap.gapId} · ${gap.reason} · repair ${gap.lastMethod ?? "—"}`;
|
||||
item.append(top, meta);
|
||||
gaps.append(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
frontendTrace("main", "Raw Transaction Ingest Desk route monitoring detail rendered", {
|
||||
gapCount: monitoring.gaps.length,
|
||||
routeId: monitoring.routeId,
|
||||
sequence: monitoring.sequence,
|
||||
state: monitoring.state,
|
||||
});
|
||||
}
|
||||
|
||||
function renderRouteMonitoringCard(status: RawIngestRouteMonitoringDto): boolean {
|
||||
const key = runtimeKey(status.profileId, status.routeId);
|
||||
const card = routeCards.get(key);
|
||||
if (!card) {
|
||||
return false;
|
||||
}
|
||||
const field = (name: string): HTMLElement | null => card.querySelector<HTMLElement>(`[data-monitoring-field="${name}"]`);
|
||||
const state = field("state");
|
||||
const health = field("health");
|
||||
const activity = field("activity");
|
||||
const persisted = field("persisted");
|
||||
const sources = field("sources");
|
||||
const gaps = field("gaps");
|
||||
if (!state || !health || !activity || !persisted || !sources || !gaps) {
|
||||
return false;
|
||||
}
|
||||
state.className = lifecycleBadgeClass(status.state);
|
||||
state.textContent = status.state;
|
||||
health.className = status.health === "healthy" ? "badge text-bg-success" : "badge text-bg-warning";
|
||||
health.textContent = `health: ${status.health}`;
|
||||
activity.textContent = `activity: ${status.activity}`;
|
||||
persisted.textContent = `persisted: ${status.persistedTotal}`;
|
||||
sources.textContent = `sources: ${status.sourceActive}/${status.sourceTotal}`;
|
||||
gaps.className = status.openGapCount === 0 ? "badge text-bg-light border text-dark" : "badge text-bg-warning";
|
||||
gaps.textContent = `gaps: ${status.openGapCount}`;
|
||||
return true;
|
||||
}
|
||||
|
||||
function selectedCommitment(): RawIngestCommitment {
|
||||
const selector = document.querySelector<HTMLSelectElement>("#routeCommitment");
|
||||
return selector?.value === "finalized" ? "finalized" : "confirmed";
|
||||
}
|
||||
|
||||
function runtimeIsActive(runtime: RawIngestRouteRuntimeDto): boolean {
|
||||
return runtime.state === "starting" || runtime.state === "running" || runtime.state === "stopping";
|
||||
}
|
||||
|
||||
function renderMultiRouteRuntime(): void {
|
||||
const active = Array.from(activeRuntimes.values());
|
||||
const state = document.querySelector<HTMLElement>("#routeRuntimeState");
|
||||
if (state) {
|
||||
state.textContent = active.length === 0 ? "idle" : `${active.length} active`;
|
||||
state.className = active.length === 0 ? "badge text-bg-secondary" : "badge text-bg-primary";
|
||||
}
|
||||
const identities = active.map(runtime => `${runtime.routeId}/${runtime.commitment}`).join(", ");
|
||||
text("#routeRuntimeIdentity", identities.length === 0 ? "aucun Worker actif" : identities);
|
||||
const store = document.querySelector<HTMLElement>("#headerStore");
|
||||
if (store) {
|
||||
store.textContent = active.length === 0 ? "Store idle" : "Store shared";
|
||||
store.className = active.length === 0 ? "badge text-bg-secondary" : "badge text-bg-success";
|
||||
}
|
||||
const profile = document.querySelector<HTMLSelectElement>("#routeProfile");
|
||||
if (profile) {
|
||||
profile.disabled = active.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
async function startRoute(routeId: RawIngestRouteId): Promise<void> {
|
||||
if (routeInventory === null || selectedProfileId === null) {
|
||||
return;
|
||||
}
|
||||
const key = runtimeKey(selectedProfileId, routeId);
|
||||
const existing = activeRuntimes.get(key);
|
||||
if (existing !== undefined && runtimeIsActive(existing)) {
|
||||
return;
|
||||
}
|
||||
const request: RawIngestRouteStartRequestDto = {
|
||||
commitment: selectedCommitment(),
|
||||
inventoryGeneration: routeInventory.generation,
|
||||
profileId: selectedProfileId,
|
||||
routeId,
|
||||
};
|
||||
frontendDebug("main", "Raw Transaction Ingest Desk route Start requested", {
|
||||
commitment: request.commitment,
|
||||
profileId: request.profileId,
|
||||
routeId: request.routeId,
|
||||
});
|
||||
const response = await invokeKsp<RawIngestRouteRuntimeDto>("main", "start_route", { request });
|
||||
lastRuntimes.set(key, response);
|
||||
if (runtimeIsActive(response)) {
|
||||
activeRuntimes.set(key, response);
|
||||
} else {
|
||||
activeRuntimes.delete(key);
|
||||
}
|
||||
renderMultiRouteRuntime();
|
||||
renderSelectedProfile();
|
||||
renderRouteFeedback(`Route ${response.routeId} démarrée (${response.state}).`, "success");
|
||||
await syncRouteMonitoring("start");
|
||||
frontendInfo("main", "Raw Transaction Ingest Desk route Start acknowledged", {
|
||||
activeRouteCount: activeRuntimes.size,
|
||||
profileId: response.profileId,
|
||||
routeId: response.routeId,
|
||||
state: response.state,
|
||||
});
|
||||
}
|
||||
|
||||
async function stopRoute(runtime: RawIngestRouteRuntimeDto): Promise<void> {
|
||||
const key = runtimeKey(runtime.profileId, runtime.routeId);
|
||||
const request: RawIngestRouteStopRequestDto = {
|
||||
profileId: runtime.profileId,
|
||||
routeId: runtime.routeId,
|
||||
};
|
||||
frontendDebug("main", "Raw Transaction Ingest Desk targeted route Stop requested", {
|
||||
profileId: request.profileId,
|
||||
routeId: request.routeId,
|
||||
});
|
||||
const response = await invokeKsp<RawIngestRouteRuntimeDto>("main", "stop_route", { request });
|
||||
lastRuntimes.set(key, response);
|
||||
if (runtimeIsActive(response)) {
|
||||
activeRuntimes.set(key, response);
|
||||
} else {
|
||||
activeRuntimes.delete(key);
|
||||
}
|
||||
renderMultiRouteRuntime();
|
||||
renderSelectedProfile();
|
||||
renderRouteFeedback(`Route ${response.routeId} arrêtée (${response.state}).`, "info");
|
||||
await syncRouteMonitoring("stop");
|
||||
frontendInfo("main", "Raw Transaction Ingest Desk targeted route Stop completed", {
|
||||
activeRouteCount: activeRuntimes.size,
|
||||
profileId: response.profileId,
|
||||
routeId: response.routeId,
|
||||
state: response.state,
|
||||
});
|
||||
}
|
||||
|
||||
function applyRouteMonitoring(status: RawIngestRouteMonitoringDto, source: "event" | "startup" | "start" | "stop" | "user"): void {
|
||||
const key = runtimeKey(status.profileId, status.routeId);
|
||||
const previous = routeMonitoring.get(key) ?? null;
|
||||
recordRouteMonitoring(status);
|
||||
if (selectedMonitoringKey === null && status.profileId === selectedProfileId) {
|
||||
selectedMonitoringKey = key;
|
||||
}
|
||||
renderMultiRouteRuntime();
|
||||
const lifecycleChanged = previous === null || previous.state !== status.state;
|
||||
if (lifecycleChanged || !renderRouteMonitoringCard(status)) {
|
||||
renderSelectedProfile();
|
||||
} else if (selectedMonitoringKey === key) {
|
||||
renderMonitoringDetail();
|
||||
}
|
||||
frontendTrace("main", "Raw Transaction Ingest Desk route monitoring latest value applied", {
|
||||
lifecycleChanged,
|
||||
routeId: status.routeId,
|
||||
sequence: status.sequence,
|
||||
source,
|
||||
state: status.state,
|
||||
terminal: status.terminal,
|
||||
});
|
||||
}
|
||||
|
||||
async function syncRouteMonitoring(source: "startup" | "start" | "stop" | "user"): Promise<void> {
|
||||
frontendDebug("main", "Raw Transaction Ingest Desk route monitoring resynchronization requested", { source });
|
||||
const statuses = await invokeKsp<RawIngestRouteMonitoringDto[]>("main", "get_route_monitoring");
|
||||
routeMonitoring.clear();
|
||||
activeRuntimes.clear();
|
||||
lastRuntimes.clear();
|
||||
for (const status of statuses) {
|
||||
recordRouteMonitoring(status);
|
||||
}
|
||||
if (selectedMonitoringKey !== null && !routeMonitoring.has(selectedMonitoringKey)) {
|
||||
selectedMonitoringKey = null;
|
||||
}
|
||||
if (selectedMonitoringKey === null) {
|
||||
const preferred = statuses.find(status => status.profileId === selectedProfileId) ?? statuses[0];
|
||||
if (preferred) {
|
||||
selectedMonitoringKey = runtimeKey(preferred.profileId, preferred.routeId);
|
||||
}
|
||||
}
|
||||
renderMultiRouteRuntime();
|
||||
renderSelectedProfile();
|
||||
frontendDebug("main", "Raw Transaction Ingest Desk route monitoring resynchronization completed", {
|
||||
activeRouteCount: activeRuntimes.size,
|
||||
source,
|
||||
statusCount: statuses.length,
|
||||
});
|
||||
}
|
||||
|
||||
async function bindRouteMonitoring(): Promise<void> {
|
||||
await listen<RawIngestRouteMonitoringDto>("ksp-raw-ingest-route-status", event => {
|
||||
applyRouteMonitoring(event.payload, "event");
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#refreshRouteMonitoring")?.addEventListener("click", () => {
|
||||
frontendDebug("main", "Raw Transaction Ingest Desk route monitoring refresh button clicked");
|
||||
void syncRouteMonitoring("user").catch(caughtError => {
|
||||
renderRouteFeedback(safeCommandErrorText(caughtError, "La resynchronisation du monitoring a échoué."), "danger");
|
||||
frontendWarn("main", "Raw Transaction Ingest Desk route monitoring resynchronization failed");
|
||||
});
|
||||
});
|
||||
frontendTrace("main", "Raw Transaction Ingest Desk route monitoring listener and resynchronization control installed");
|
||||
}
|
||||
|
||||
function renderRouteInventory(inventory: RawIngestRouteInventoryDto): void {
|
||||
routeInventory = inventory;
|
||||
const requestedProfile = selectedProfileId;
|
||||
const selectedStillExists = requestedProfile !== null && inventory.profiles.some(profile => profile.profileId === requestedProfile);
|
||||
selectedProfileId = selectedStillExists ? requestedProfile : inventory.defaultProfile;
|
||||
const selector = document.querySelector<HTMLSelectElement>("#routeProfile");
|
||||
if (selector) {
|
||||
selector.replaceChildren();
|
||||
for (const profile of inventory.profiles) {
|
||||
const option = document.createElement("option");
|
||||
option.value = profile.profileId;
|
||||
option.textContent = profile.isDefault ? `${profile.profileId} (default)` : profile.profileId;
|
||||
option.selected = profile.profileId === selectedProfileId;
|
||||
selector.append(option);
|
||||
}
|
||||
}
|
||||
text("#routeInventoryGeneration", inventory.generation.toString());
|
||||
text("#runtimeInventoryGeneration", inventory.generation.toString());
|
||||
text("#runtimeInventoryDefaultProfile", inventory.defaultProfile);
|
||||
renderSelectedProfile();
|
||||
renderMultiRouteRuntime();
|
||||
frontendTrace("main", "Raw Transaction Ingest Desk Config route inventory rendered", {
|
||||
generation: inventory.generation,
|
||||
profileCount: inventory.profiles.length,
|
||||
});
|
||||
}
|
||||
|
||||
function text(selector: string, value: string): void {
|
||||
const element = document.querySelector<HTMLElement>(selector);
|
||||
if (element) {
|
||||
element.textContent = value;
|
||||
}
|
||||
}
|
||||
|
||||
function renderRuntimeStatus(status: ShellStatusDto): void {
|
||||
text("#runtimeVersion", status.applicationVersion);
|
||||
text("#runtimeShellPhase", status.shellPhase);
|
||||
text("#runtimeConfigDocuments", status.configDocumentCount.toString());
|
||||
text("#runtimeCompositeProfile", status.activeCompositeProfile ?? "fallback/unresolved");
|
||||
text("#runtimeLoggingProfile", status.activeLoggingProfile ?? "fallback");
|
||||
text("#runtimeLoggingFallback", status.fallbackLoggingActive ? "oui" : "non");
|
||||
text("#headerPhase", status.shellPhase);
|
||||
const diagnostic = document.querySelector<HTMLElement>("#runtimeDiagnostic");
|
||||
if (diagnostic) {
|
||||
diagnostic.hidden = status.startupDiagnostic === null;
|
||||
diagnostic.textContent = status.startupDiagnostic ? `${status.startupDiagnostic.domain}/${status.startupDiagnostic.code}: ${status.startupDiagnostic.message}` : "";
|
||||
}
|
||||
frontendTrace("main", "Raw Transaction Ingest Desk runtime status rendered", {
|
||||
fallbackLoggingActive: status.fallbackLoggingActive,
|
||||
hasStartupDiagnostic: status.startupDiagnostic !== null,
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshRouteInventory(): Promise<void> {
|
||||
frontendDebug("main", "Raw Transaction Ingest Desk Config route inventory refresh requested");
|
||||
const inventory = await invokeKsp<RawIngestRouteInventoryDto>("main", "get_route_inventory");
|
||||
renderRouteInventory(inventory);
|
||||
}
|
||||
|
||||
async function refreshRuntimeStatus(): Promise<void> {
|
||||
frontendDebug("main", "Raw Transaction Ingest Desk runtime status refresh requested");
|
||||
const status = await invokeKsp<ShellStatusDto>("main", "get_runtime_status");
|
||||
renderRuntimeStatus(status);
|
||||
}
|
||||
|
||||
function bindRefreshControls(): void {
|
||||
document.querySelector<HTMLButtonElement>("#refreshRouteInventory")?.addEventListener("click", () => {
|
||||
void refreshRouteInventory().catch(() => frontendWarn("main", "Raw Transaction Ingest Desk Config route inventory refresh failed"));
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#refreshRuntimeStatus")?.addEventListener("click", () => {
|
||||
void refreshRuntimeStatus().catch(() => frontendWarn("main", "Raw Transaction Ingest Desk runtime status refresh failed"));
|
||||
});
|
||||
document.querySelector<HTMLSelectElement>("#routeProfile")?.addEventListener("change", event => {
|
||||
const selector = event.currentTarget;
|
||||
if (!(selector instanceof HTMLSelectElement)) {
|
||||
return;
|
||||
}
|
||||
selectedProfileId = selector.value;
|
||||
selectedMonitoringKey = null;
|
||||
const selected = routeInventory?.profiles.find(profile => profile.profileId === selectedProfileId) ?? null;
|
||||
frontendDebug("main", "Raw Transaction Ingest Desk logical network selected", {
|
||||
profileId: selectedProfileId,
|
||||
routeCount: selected?.routes.length ?? 0,
|
||||
selectionResolved: selected !== null,
|
||||
});
|
||||
renderSelectedProfile();
|
||||
});
|
||||
document.querySelector<HTMLSelectElement>("#routeCommitment")?.addEventListener("change", event => {
|
||||
const selector = event.currentTarget;
|
||||
if (!(selector instanceof HTMLSelectElement)) {
|
||||
return;
|
||||
}
|
||||
frontendDebug("main", "Raw Transaction Ingest Desk commitment selection changed", { commitment: selectedCommitment() });
|
||||
});
|
||||
}
|
||||
|
||||
async function initializeMain(): Promise<void> {
|
||||
frontendInfo("main", "Raw Transaction Ingest Desk main frontend loaded");
|
||||
bindFrontendInteractions();
|
||||
bindNavigation();
|
||||
bindRefreshControls();
|
||||
await bindRouteMonitoring();
|
||||
activateView("routes", "startup");
|
||||
await refreshRouteInventory();
|
||||
await Promise.all([refreshRuntimeStatus(), syncRouteMonitoring("startup")]);
|
||||
frontendInfo("main", "Raw Transaction Ingest Desk multi-route shared-Store runtime frontend ready");
|
||||
}
|
||||
|
||||
void initializeMain().catch(() => frontendWarn("main", "Raw Transaction Ingest Desk frontend initialization failed"));
|
||||
131
crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/splash.ts
Normal file
131
crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/splash.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/splash.ts
|
||||
// version: 1
|
||||
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import type { SplashOrderDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/splash/SplashOrderDto.ts";
|
||||
import { frontendDebug, frontendError, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log";
|
||||
import { invokeKsp } from "./invoke";
|
||||
|
||||
installFrontendConsoleBridge("splash");
|
||||
|
||||
let activeOpacityFrame: number | null = null;
|
||||
|
||||
function easeInOut(value: number): number {
|
||||
return value < 0.5 ? 2 * value * value : 1 - Math.pow(-2 * value + 2, 2) / 2;
|
||||
}
|
||||
|
||||
function normalizeDurationMs(value: number | null): number {
|
||||
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
||||
}
|
||||
|
||||
async function animateOpacity(element: HTMLElement, fromOpacity: number, toOpacity: number, durationMs: number): Promise<void> {
|
||||
frontendTrace("splash", "Splash opacity animation started", { fromOpacity, toOpacity, durationMs });
|
||||
if (activeOpacityFrame !== null) {
|
||||
cancelAnimationFrame(activeOpacityFrame);
|
||||
activeOpacityFrame = null;
|
||||
}
|
||||
element.style.opacity = fromOpacity.toString();
|
||||
element.style.willChange = "opacity";
|
||||
await new Promise<void>(resolve => requestAnimationFrame(() => resolve()));
|
||||
await new Promise<void>(resolve => {
|
||||
const startedAt = performance.now();
|
||||
const opacityDelta = toOpacity - fromOpacity;
|
||||
const updateOpacity = (currentTime: number): void => {
|
||||
const elapsedMs = currentTime - startedAt;
|
||||
const rawProgress = durationMs === 0 ? 1 : Math.min(elapsedMs / durationMs, 1);
|
||||
element.style.opacity = (fromOpacity + opacityDelta * easeInOut(rawProgress)).toString();
|
||||
if (rawProgress >= 1) {
|
||||
element.style.opacity = toOpacity.toString();
|
||||
element.style.willChange = "auto";
|
||||
activeOpacityFrame = null;
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
activeOpacityFrame = requestAnimationFrame(updateOpacity);
|
||||
};
|
||||
activeOpacityFrame = requestAnimationFrame(updateOpacity);
|
||||
});
|
||||
frontendTrace("splash", "Splash opacity animation completed", { toOpacity, durationMs });
|
||||
}
|
||||
|
||||
function scrollToLatest(element: HTMLElement): void {
|
||||
element.scrollTop = element.scrollHeight;
|
||||
}
|
||||
|
||||
function addMessage(message: string, status: string | null): void {
|
||||
const container = document.querySelector<HTMLElement>("#messages-container");
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
const line = document.createElement("div");
|
||||
const safeStatus = status === "warning" || status === "error" || status === "success" ? status : "info";
|
||||
line.className = `splash-message ${safeStatus}`;
|
||||
line.textContent = message;
|
||||
container.appendChild(line);
|
||||
scrollToLatest(container);
|
||||
}
|
||||
|
||||
function addDebugMessage(message: string): void {
|
||||
const container = document.querySelector<HTMLElement>("#debug-info");
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
container.hidden = false;
|
||||
const line = document.createElement("div");
|
||||
line.textContent = `${new Date().toLocaleTimeString()}: ${message}`;
|
||||
container.appendChild(line);
|
||||
scrollToLatest(container);
|
||||
}
|
||||
|
||||
async function handleSplashOrder(order: SplashOrderDto): Promise<void> {
|
||||
frontendTrace("splash", "Splash order received", { action: order.action });
|
||||
const container = document.querySelector<HTMLElement>("#splash-container");
|
||||
if (order.action === "add_message" && order.message) {
|
||||
addMessage(order.message, order.status);
|
||||
return;
|
||||
}
|
||||
if (order.action === "add_debug" && order.message) {
|
||||
addDebugMessage(order.message);
|
||||
return;
|
||||
}
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
if (order.action === "fade_in") {
|
||||
await animateOpacity(container, 0, 1, normalizeDurationMs(order.durationMs));
|
||||
return;
|
||||
}
|
||||
if (order.action === "fade_out") {
|
||||
await animateOpacity(container, 1, 0, normalizeDurationMs(order.durationMs));
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeSplash(): Promise<void> {
|
||||
const windowLabel = getCurrentWindow().label;
|
||||
frontendInfo("splash", "Raw Transaction Ingest Desk splash frontend loaded", { windowLabel });
|
||||
const container = document.querySelector<HTMLElement>("#splash-container");
|
||||
if (container) {
|
||||
container.style.opacity = "0";
|
||||
container.style.willChange = "opacity";
|
||||
frontendTrace("splash", "Splash container prepared for managed fade-in");
|
||||
}
|
||||
await listen<SplashOrderDto>("ksp-splash-order", event => {
|
||||
void handleSplashOrder(event.payload);
|
||||
});
|
||||
frontendDebug("splash", "Splash lifecycle listener installed");
|
||||
try {
|
||||
await invokeKsp<void>("splash", "splash_frontend_ready");
|
||||
} catch {
|
||||
addMessage("Le lifecycle du splash n'a pas pu démarrer.", "error");
|
||||
if (container) {
|
||||
container.style.opacity = "1";
|
||||
container.style.willChange = "auto";
|
||||
}
|
||||
frontendError("splash", "Splash readiness command failed");
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
void initializeSplash();
|
||||
});
|
||||
BIN
crates/ksp-app-raw-transaction-ingest-desk/icons/favicon.ico
Normal file
BIN
crates/ksp-app-raw-transaction-ingest-desk/icons/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 448 KiB |
BIN
crates/ksp-app-raw-transaction-ingest-desk/icons/favicon.png
Normal file
BIN
crates/ksp-app-raw-transaction-ingest-desk/icons/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
26
crates/ksp-app-raw-transaction-ingest-desk/package.json
Normal file
26
crates/ksp-app-raw-transaction-ingest-desk/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "ksp-app-raw-transaction-ingest-desk",
|
||||
"private": true,
|
||||
"version": "0.3.15",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fltsci/tauri-plugin-tracing": "^0.3",
|
||||
"@fortawesome/fontawesome-free": "^7.3",
|
||||
"@tauri-apps/api": "^2.11",
|
||||
"bootstrap": "^5.3",
|
||||
"resize-observer-polyfill": "^1.5",
|
||||
"simplebar": "^6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.11",
|
||||
"@types/bootstrap": "^5.2",
|
||||
"@types/node": "^26.1",
|
||||
"sass-embedded": "^1.102",
|
||||
"typescript": "^7.0",
|
||||
"vite": "^8.2"
|
||||
}
|
||||
}
|
||||
222
crates/ksp-app-raw-transaction-ingest-desk/src/app_state.rs
Normal file
222
crates/ksp-app-raw-transaction-ingest-desk/src/app_state.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/app_state.rs
|
||||
// version: 9
|
||||
|
||||
//! Shared backend state owned by the Raw Transaction Ingest Desk Tauri application.
|
||||
|
||||
/// Shared Raw Transaction Ingest Desk application state managed by Tauri.
|
||||
pub(crate) struct AppState {
|
||||
active_composite_profile: std::option::Option<String>,
|
||||
config_management: ksp_config_lib::ConfigManagement,
|
||||
inventory_generation: std::sync::Mutex<u32>,
|
||||
logging_runtime: std::sync::Mutex<LoggingRuntimeState>,
|
||||
route_runtime: std::sync::Arc<crate::RouteRuntimeState>,
|
||||
splash_settings: crate::SplashSettings,
|
||||
splash_sequence_started: std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
impl crate::AppState {
|
||||
/// Initializes Config ownership and composite-managed Logging without constructing Transport, Store or Worker resources.
|
||||
pub(crate) fn initialize(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<Self> {
|
||||
let config_management = crate::config_management(arguments);
|
||||
let config_management = match config_management {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let runtime_identity = crate::launch_identity();
|
||||
let runtime_identity = match runtime_identity {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let logging_startup = crate::initialize_logging(&config_management, &runtime_identity);
|
||||
let logging_startup = match logging_startup {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let composite = crate::load_raw_transaction_ingest_desk_composite(&config_management);
|
||||
let active_composite_profile = match composite {
|
||||
std::result::Result::Ok(value) => std::option::Option::Some(value.profile_id().to_owned()),
|
||||
std::result::Result::Err(_) => std::option::Option::None,
|
||||
};
|
||||
let splash_settings = crate::SplashSettings::load();
|
||||
let splash_settings = match splash_settings {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, error_domain = error.code().domain(), error_code = error.code().code(), "managed splash timings are invalid; using transient in-memory defaults");
|
||||
crate::SplashSettings::fallback()
|
||||
},
|
||||
};
|
||||
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, minimum_ms = splash_settings.minimum_ms(), minimum_source = splash_settings.minimum_source(), fade_in_ms = splash_settings.fade_in_ms(), fade_in_source = splash_settings.fade_in_source(), fade_out_ms = splash_settings.fade_out_ms(), fade_out_source = splash_settings.fade_out_source(), expected_backend_lifecycle_ms = splash_settings.expected_backend_lifecycle_ms(), "resolved Raw Transaction Ingest Desk splash timings");
|
||||
return std::result::Result::Ok(Self {
|
||||
active_composite_profile,
|
||||
config_management,
|
||||
inventory_generation: std::sync::Mutex::new(0),
|
||||
logging_runtime: std::sync::Mutex::new(LoggingRuntimeState {
|
||||
guard: logging_startup.guard,
|
||||
active_profile_id: logging_startup.active_profile_id,
|
||||
fallback_active: logging_startup.fallback_active,
|
||||
startup_diagnostic: logging_startup.startup_diagnostic,
|
||||
}),
|
||||
route_runtime: std::sync::Arc::new(crate::RouteRuntimeState::new()),
|
||||
splash_settings,
|
||||
splash_sequence_started: std::sync::atomic::AtomicBool::new(false),
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the static V1 route/state vocabulary without making any Config composability claim.
|
||||
#[must_use]
|
||||
pub(crate) fn route_foundation(&self) -> crate::RawIngestRouteFoundationDto {
|
||||
let _keep_config_owner_alive = &self.config_management;
|
||||
return crate::RawIngestRouteFoundationDto::scaffold();
|
||||
}
|
||||
|
||||
/// Rebuilds the complete Config-only route inventory and publishes a fresh monotonic generation.
|
||||
pub(crate) fn route_inventory(&self) -> ksp_core_lib::Result<crate::RawIngestRouteInventoryDto> {
|
||||
let generation = self.inventory_generation.lock();
|
||||
let mut generation = match generation {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
|
||||
"Raw Transaction Ingest Desk route inventory generation lock is poisoned",
|
||||
));
|
||||
},
|
||||
};
|
||||
let next_generation = generation.checked_add(1);
|
||||
let next_generation = match next_generation {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_INVENTORY_INVALID,
|
||||
"Raw Transaction Ingest Desk route inventory generation is exhausted",
|
||||
));
|
||||
},
|
||||
};
|
||||
let inventory = crate::build_route_inventory(&self.config_management, next_generation);
|
||||
let inventory = match inventory {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
*generation = next_generation;
|
||||
return std::result::Result::Ok(inventory);
|
||||
}
|
||||
|
||||
/// Revalidates one future Start request and reconstructs its exact Transport/Worker source contract without launching a Worker.
|
||||
pub(crate) fn validate_route_start(&self, request: &crate::RawIngestRouteStartRequestDto) -> ksp_core_lib::Result<crate::RawIngestRouteStartValidationDto> {
|
||||
let generation = self.inventory_generation.lock();
|
||||
let generation = match generation {
|
||||
std::result::Result::Ok(value) => *value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
|
||||
"Raw Transaction Ingest Desk route inventory generation lock is poisoned",
|
||||
));
|
||||
},
|
||||
};
|
||||
return crate::validate_route_start(&self.config_management, generation, request);
|
||||
}
|
||||
|
||||
/// Starts one independent route Worker after repeating complete Start-time revalidation, sharing Store only on an exact network match.
|
||||
pub(crate) async fn start_route(&self, request: &crate::RawIngestRouteStartRequestDto) -> ksp_core_lib::Result<crate::RouteRuntimeLaunch> {
|
||||
let admission = self.route_runtime.ensure_start_admission_open();
|
||||
if let std::result::Result::Err(error) = admission {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let prepared = {
|
||||
let generation = self.inventory_generation.lock();
|
||||
let generation = match generation {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
|
||||
"Raw Transaction Ingest Desk route inventory generation lock is poisoned",
|
||||
));
|
||||
},
|
||||
};
|
||||
let prepared = crate::prepare_route_start(&self.config_management, *generation, request);
|
||||
match prepared {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
};
|
||||
return crate::start_route_runtime(std::sync::Arc::clone(&self.route_runtime), prepared).await;
|
||||
}
|
||||
|
||||
/// Returns complete latest-value monitoring projections for active and recent terminal routes in the current same-network runtime session.
|
||||
pub(crate) fn route_monitoring(&self) -> ksp_core_lib::Result<std::vec::Vec<crate::RawIngestRouteMonitoringDto>> {
|
||||
return self.route_runtime.monitoring_statuses();
|
||||
}
|
||||
|
||||
/// Requests cooperative Stop for one exact logical route and waits until its Worker cleanup is complete.
|
||||
pub(crate) async fn stop_route(&self, request: &crate::RawIngestRouteStopRequestDto) -> ksp_core_lib::Result<crate::RawIngestRouteRuntimeDto> {
|
||||
return self.route_runtime.stop_and_wait(request).await;
|
||||
}
|
||||
|
||||
/// Closes route Start admission once and requests cooperative Stop for all routes during application shutdown.
|
||||
pub(crate) fn begin_shutdown(&self) -> ksp_core_lib::Result<bool> {
|
||||
return self.route_runtime.begin_shutdown();
|
||||
}
|
||||
|
||||
/// Waits for bounded cleanup of all route Workers and the shared Store during application shutdown.
|
||||
pub(crate) async fn shutdown_routes(&self) -> ksp_core_lib::Result<()> {
|
||||
return self.route_runtime.shutdown_and_wait().await;
|
||||
}
|
||||
|
||||
/// Builds the safe scaffold status exposed by the shell.
|
||||
pub(crate) fn shell_status(&self) -> ksp_core_lib::Result<crate::ShellStatusDto> {
|
||||
let document_count = self.config_management.engine().registry().descriptors().count();
|
||||
let document_count = u32::try_from(document_count);
|
||||
let document_count = match document_count {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_APP_STATE_INVALID,
|
||||
"Config registry contains too many descriptors for the Raw Transaction Ingest Desk runtime DTO",
|
||||
)
|
||||
.with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
let runtime = self.logging_runtime.lock();
|
||||
let runtime = match runtime {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
|
||||
"Raw Transaction Ingest Desk Logging runtime state lock is poisoned",
|
||||
));
|
||||
},
|
||||
};
|
||||
let _keep_guard_alive = &runtime.guard;
|
||||
return std::result::Result::Ok(crate::ShellStatusDto {
|
||||
active_composite_profile: self.active_composite_profile.clone(),
|
||||
active_logging_profile: runtime.active_profile_id.clone(),
|
||||
application_version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
config_document_count: document_count,
|
||||
fallback_logging_active: runtime.fallback_active,
|
||||
shell_phase: "pre.010-runtime-monitoring".to_owned(),
|
||||
startup_diagnostic: runtime.startup_diagnostic.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the resolved splash timings captured during application bootstrap.
|
||||
#[must_use]
|
||||
pub(crate) const fn splash_settings(&self) -> crate::SplashSettings {
|
||||
return self.splash_settings;
|
||||
}
|
||||
|
||||
/// Marks the one-shot splash lifecycle as started and reports whether this caller won the transition.
|
||||
pub(crate) fn begin_splash_sequence(&self) -> bool {
|
||||
return self
|
||||
.splash_sequence_started
|
||||
.compare_exchange(false, true, std::sync::atomic::Ordering::AcqRel, std::sync::atomic::Ordering::Acquire)
|
||||
.is_ok();
|
||||
}
|
||||
}
|
||||
|
||||
struct LoggingRuntimeState {
|
||||
guard: ksp_logging_lib::LoggingGuard,
|
||||
active_profile_id: std::option::Option<String>,
|
||||
fallback_active: bool,
|
||||
startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
|
||||
}
|
||||
228
crates/ksp-app-raw-transaction-ingest-desk/src/bootstrap.rs
Normal file
228
crates/ksp-app-raw-transaction-ingest-desk/src/bootstrap.rs
Normal file
@@ -0,0 +1,228 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/bootstrap.rs
|
||||
// version: 2
|
||||
|
||||
//! Config composite and Logging bootstrap for Raw Transaction Ingest Desk.
|
||||
|
||||
/// Crate-internal Logging startup state shared by the application state.
|
||||
pub(crate) struct LoggingStartup {
|
||||
/// Guard that keeps the installed Logging runtime alive.
|
||||
pub(crate) guard: ksp_logging_lib::LoggingGuard,
|
||||
/// Active managed profile when Config resolved one successfully.
|
||||
pub(crate) active_profile_id: std::option::Option<String>,
|
||||
/// Whether a transient in-memory fallback was installed.
|
||||
pub(crate) fallback_active: bool,
|
||||
/// Safe startup diagnostic retained for the shell.
|
||||
pub(crate) startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
|
||||
}
|
||||
|
||||
enum LoggingStartupPlan {
|
||||
Managed {
|
||||
active_profile_id: String,
|
||||
settings: ksp_logging_lib::LoggingSettings,
|
||||
},
|
||||
Fallback {
|
||||
initial_error: ksp_core_lib::Error,
|
||||
diagnostic: crate::CommandErrorDto,
|
||||
settings: ksp_logging_lib::LoggingSettings,
|
||||
},
|
||||
}
|
||||
|
||||
/// Builds the Config management facade from the common KSP CLI bootstrap contract.
|
||||
pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<ksp_config_lib::ConfigManagement> {
|
||||
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_args(arguments);
|
||||
let bootstrap = match bootstrap {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let registry = ksp_config_lib::ConfigFileRegistry::from_args(arguments);
|
||||
let registry = match registry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let engine = ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry);
|
||||
return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(engine));
|
||||
}
|
||||
|
||||
/// Loads the concrete Raw Transaction Ingest Desk Config composite using its registered logical `file_id` and autonomous default profile.
|
||||
pub(crate) fn load_raw_transaction_ingest_desk_composite(
|
||||
management: &ksp_config_lib::ConfigManagement,
|
||||
) -> ksp_core_lib::Result<ksp_config_lib::ResolvedConfigComposite> {
|
||||
return crate::load_raw_transaction_ingest_desk_composite_profile(management, std::option::Option::None);
|
||||
}
|
||||
|
||||
/// Loads one explicit or default Raw Transaction Ingest Desk composite profile by registered logical file identifier.
|
||||
pub(crate) fn load_raw_transaction_ingest_desk_composite_profile(
|
||||
management: &ksp_config_lib::ConfigManagement,
|
||||
requested_profile: std::option::Option<&str>,
|
||||
) -> ksp_core_lib::Result<ksp_config_lib::ResolvedConfigComposite> {
|
||||
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK);
|
||||
let file_id = match file_id {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return management.engine().load_resolved_composite(&file_id, requested_profile);
|
||||
}
|
||||
|
||||
/// Returns one required standard profile from the Raw Transaction Ingest Desk composite after validating the referenced logical `file_id`.
|
||||
pub(crate) fn required_composite_component_profile(
|
||||
composite: &ksp_config_lib::ResolvedConfigComposite,
|
||||
component_id: &str,
|
||||
expected_file_id: &str,
|
||||
) -> ksp_core_lib::Result<ksp_config_lib::ResolvedConfigProfile> {
|
||||
let component = composite.component(component_id);
|
||||
let component = match component {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_CONFIG_COMPOSITE_INVALID, "Raw Transaction Ingest Desk composite is missing a required component")
|
||||
.with_context("composite_file_id", composite.file_id().as_str())
|
||||
.with_context("composite_profile_id", composite.profile_id())
|
||||
.with_context("component_id", component_id),
|
||||
);
|
||||
},
|
||||
};
|
||||
if component.resolved().file_id().as_str() != expected_file_id {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_CONFIG_COMPOSITE_INVALID,
|
||||
"Raw Transaction Ingest Desk composite component references an unexpected Config document",
|
||||
)
|
||||
.with_context("composite_file_id", composite.file_id().as_str())
|
||||
.with_context("composite_profile_id", composite.profile_id())
|
||||
.with_context("component_id", component_id)
|
||||
.with_context("expected_file_id", expected_file_id)
|
||||
.with_context("actual_file_id", component.resolved().file_id().as_str()),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(component.resolved().clone());
|
||||
}
|
||||
|
||||
/// Initializes Logging from the Raw Transaction Ingest Desk composite, with a trace-level development fallback.
|
||||
pub(crate) fn initialize_logging(
|
||||
management: &ksp_config_lib::ConfigManagement,
|
||||
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
|
||||
) -> ksp_core_lib::Result<crate::LoggingStartup> {
|
||||
let plan = resolve_logging_startup(management);
|
||||
return match plan {
|
||||
LoggingStartupPlan::Managed { active_profile_id, settings } => {
|
||||
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
|
||||
match guard {
|
||||
std::result::Result::Ok(guard) => {
|
||||
ksp_logging_lib::info!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_BOOTSTRAP,
|
||||
active_profile = active_profile_id.as_str(),
|
||||
"initialized Raw Transaction Ingest Desk logging from composite-managed configuration"
|
||||
);
|
||||
std::result::Result::Ok(LoggingStartup {
|
||||
guard,
|
||||
active_profile_id: std::option::Option::Some(active_profile_id),
|
||||
fallback_active: false,
|
||||
startup_diagnostic: std::option::Option::None,
|
||||
})
|
||||
},
|
||||
std::result::Result::Err(error) => initialize_fallback_logging(error, runtime_identity),
|
||||
}
|
||||
},
|
||||
LoggingStartupPlan::Fallback { initial_error, diagnostic, settings } => {
|
||||
initialize_planned_fallback_logging(initial_error, diagnostic, settings, runtime_identity)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn fallback_logging_settings() -> ksp_logging_lib::LoggingSettings {
|
||||
return ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Trace,
|
||||
ksp_logging_lib::SpanEvents::Off,
|
||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
||||
std::vec::Vec::new(),
|
||||
);
|
||||
}
|
||||
|
||||
fn resolve_logging_startup(management: &ksp_config_lib::ConfigManagement) -> LoggingStartupPlan {
|
||||
let environment = ksp_config_lib::ConfigEnvironment::load();
|
||||
let environment = match environment {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return fallback_startup_plan(error),
|
||||
};
|
||||
let composite = crate::load_raw_transaction_ingest_desk_composite(management);
|
||||
let composite = match composite {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return fallback_startup_plan(error),
|
||||
};
|
||||
let logging_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_LOGGING, ksp_config_lib::FILE_ID_STD_LOGGING);
|
||||
let logging_profile = match logging_profile {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return fallback_startup_plan(error),
|
||||
};
|
||||
let transport_profile =
|
||||
crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT, ksp_config_lib::FILE_ID_STD_TRANSPORT);
|
||||
if let std::result::Result::Err(error) = transport_profile {
|
||||
return fallback_startup_plan(error);
|
||||
}
|
||||
let store_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_STORE, ksp_config_lib::FILE_ID_STD_STORE);
|
||||
if let std::result::Result::Err(error) = store_profile {
|
||||
return fallback_startup_plan(error);
|
||||
}
|
||||
let resolved = management.engine().resolve_logging_config_profile(&logging_profile, &environment);
|
||||
let resolved = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return fallback_startup_plan(error),
|
||||
};
|
||||
return LoggingStartupPlan::Managed { active_profile_id: resolved.profile_id().to_owned(), settings: resolved.into_settings() };
|
||||
}
|
||||
|
||||
fn fallback_startup_plan(initial_error: ksp_core_lib::Error) -> LoggingStartupPlan {
|
||||
let diagnostic = crate::CommandErrorDto::from_error(&initial_error);
|
||||
return LoggingStartupPlan::Fallback { initial_error, diagnostic, settings: fallback_logging_settings() };
|
||||
}
|
||||
|
||||
fn initialize_fallback_logging(
|
||||
initial_error: ksp_core_lib::Error,
|
||||
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
|
||||
) -> ksp_core_lib::Result<crate::LoggingStartup> {
|
||||
let diagnostic = crate::CommandErrorDto::from_error(&initial_error);
|
||||
return initialize_planned_fallback_logging(initial_error, diagnostic, fallback_logging_settings(), runtime_identity);
|
||||
}
|
||||
|
||||
fn initialize_planned_fallback_logging(
|
||||
initial_error: ksp_core_lib::Error,
|
||||
diagnostic: crate::CommandErrorDto,
|
||||
settings: ksp_logging_lib::LoggingSettings,
|
||||
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
|
||||
) -> ksp_core_lib::Result<crate::LoggingStartup> {
|
||||
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
|
||||
let guard = match guard {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(fallback_error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED, "Cannot initialize Raw Transaction Ingest Desk fallback Logging runtime")
|
||||
.with_context("initial_error_domain", initial_error.code().domain())
|
||||
.with_context("initial_error_code", initial_error.code().code())
|
||||
.with_source(fallback_error),
|
||||
);
|
||||
},
|
||||
};
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_BOOTSTRAP,
|
||||
error_domain = diagnostic.domain.as_str(),
|
||||
error_code = diagnostic.code.as_str(),
|
||||
"managed Logging configuration is unavailable; using transient trace-level development fallback"
|
||||
);
|
||||
return std::result::Result::Ok(LoggingStartup {
|
||||
guard,
|
||||
active_profile_id: std::option::Option::None,
|
||||
fallback_active: true,
|
||||
startup_diagnostic: std::option::Option::Some(diagnostic),
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn development_fallback_keeps_trace_level_enabled() {
|
||||
let settings = super::fallback_logging_settings();
|
||||
assert_eq!(settings.default_filter(), ksp_logging_lib::LogFilterLevel::Trace);
|
||||
}
|
||||
}
|
||||
39
crates/ksp-app-raw-transaction-ingest-desk/src/constants.rs
Normal file
39
crates/ksp-app-raw-transaction-ingest-desk/src/constants.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/constants.rs
|
||||
// version: 6
|
||||
|
||||
//! Application-owned tracing targets and Config component identifiers.
|
||||
|
||||
/// Composite component identifier for Logging.
|
||||
pub(crate) const COMPOSITE_COMPONENT_ID_LOGGING: &str = "logging";
|
||||
/// Composite component identifier for Store.
|
||||
pub(crate) const COMPOSITE_COMPONENT_ID_STORE: &str = "store";
|
||||
/// Composite component identifier for Transport.
|
||||
pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT: &str = "transport";
|
||||
/// Composite-local optional Transport source for Helius WebSocket capabilities.
|
||||
pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT_HELIUS: &str = "transport.helius";
|
||||
/// Prefix used by optional same-network Transport capability sources.
|
||||
pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT_PREFIX: &str = "transport.";
|
||||
/// Composite-local optional Transport source for Yellowstone gRPC capabilities.
|
||||
pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT_YELLOWSTONE: &str = "transport.yellowstone";
|
||||
/// Structured domain used while bootstrapping Config and Logging.
|
||||
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "raw_transaction_ingest.bootstrap";
|
||||
/// Structured domain used by technical frontend events.
|
||||
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
|
||||
/// Structured domain used while rebuilding Config-only route inventory.
|
||||
pub(crate) const TRACING_DOMAIN_INVENTORY: &str = "raw_transaction_ingest.inventory";
|
||||
/// Structured domain used by multi-route shared-Store and Worker lifecycle operations.
|
||||
pub(crate) const TRACING_DOMAIN_ROUTE_RUNTIME: &str = "raw_transaction_ingest.route_runtime";
|
||||
/// Structured domain used while revalidating future Start requests and reconstructing route resources.
|
||||
pub(crate) const TRACING_DOMAIN_ROUTE_START: &str = "raw_transaction_ingest.route_start";
|
||||
/// Structured domain used by the Raw Transaction Ingest Desk shell.
|
||||
pub(crate) const TRACING_DOMAIN_SHELL: &str = "raw_transaction_ingest.shell";
|
||||
/// Structured domain used by Tauri window lifecycle operations.
|
||||
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
|
||||
/// Owning target for backend events emitted by Raw Transaction Ingest Desk.
|
||||
pub(crate) const TRACING_TARGET: &str = "ksp-app-raw-transaction-ingest-desk";
|
||||
/// Owning target for generic frontend events emitted through the KSP bridge.
|
||||
pub(crate) const TRACING_TARGET_FRONTEND: &str = "ksp-app-raw-transaction-ingest-desk.frontend";
|
||||
/// Owning target for main-window frontend events.
|
||||
pub(crate) const TRACING_TARGET_FRONTEND_MAIN: &str = "ksp-app-raw-transaction-ingest-desk.frontend.main";
|
||||
/// Owning target for splash-window frontend events.
|
||||
pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-raw-transaction-ingest-desk.frontend.splash";
|
||||
56
crates/ksp-app-raw-transaction-ingest-desk/src/dto_common.rs
Normal file
56
crates/ksp-app-raw-transaction-ingest-desk/src/dto_common.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/dto_common.rs
|
||||
// version: 1
|
||||
|
||||
//! Common Tauri DTOs shared by the Raw Transaction Ingest Desk shell.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Safe command error projection that never serializes arbitrary KSP error context or source values.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_common/CommandErrorDto.ts")]
|
||||
pub(crate) struct CommandErrorDto {
|
||||
/// Stable KSP error domain.
|
||||
pub(crate) domain: String,
|
||||
/// Stable KSP error code within the domain.
|
||||
pub(crate) code: String,
|
||||
/// Human-readable error message without arbitrary context fields.
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
||||
impl crate::CommandErrorDto {
|
||||
/// Builds a bounded safe projection from a KSP error.
|
||||
#[must_use]
|
||||
pub(crate) fn from_error(error: &ksp_core_lib::Error) -> Self {
|
||||
return Self {
|
||||
domain: error.code().domain().to_owned(),
|
||||
code: error.code().code().to_owned(),
|
||||
message: error.message().to_owned(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Safe scaffold/runtime snapshot exposed to the Raw Transaction Ingest Desk shell.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_common/ShellStatusDto.ts")]
|
||||
pub(crate) struct ShellStatusDto {
|
||||
/// Active composite profile selected by Config, when the composite resolves successfully.
|
||||
pub(crate) active_composite_profile: std::option::Option<String>,
|
||||
/// Active configured Logging profile, or `None` while transient fallback Logging is active.
|
||||
pub(crate) active_logging_profile: std::option::Option<String>,
|
||||
/// Cargo application version.
|
||||
pub(crate) application_version: String,
|
||||
/// Number of logical Config resources registered by the current runtime.
|
||||
pub(crate) config_document_count: u32,
|
||||
/// Whether Raw Transaction Ingest Desk had to install its transient in-memory Logging fallback.
|
||||
pub(crate) fallback_logging_active: bool,
|
||||
/// Current implementation phase exposed by the shell.
|
||||
pub(crate) shell_phase: String,
|
||||
/// Safe startup diagnostic that caused fallback Logging, when applicable.
|
||||
pub(crate) startup_diagnostic: std::option::Option<CommandErrorDto>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/dto_common.rs"]
|
||||
mod tests;
|
||||
365
crates/ksp-app-raw-transaction-ingest-desk/src/dto_route.rs
Normal file
365
crates/ksp-app-raw-transaction-ingest-desk/src/dto_route.rs
Normal file
@@ -0,0 +1,365 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/dto_route.rs
|
||||
// version: 7
|
||||
|
||||
//! Safe route-identity, Config-composability and lifecycle DTOs for Raw Transaction Ingest Desk.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Stable logical V1 route identifiers owned by Raw Transaction Ingest Desk.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteId.ts")]
|
||||
pub(crate) enum RawIngestRouteId {
|
||||
/// Yellowstone block subscription plus same-network HTTP block reconciliation.
|
||||
YellowstoneHydrated,
|
||||
/// Standard Solana logs subscription plus same-network HTTP hydration.
|
||||
StandardLogsHydrated,
|
||||
/// Standard Solana block subscription with direct RAW projection.
|
||||
StandardBlockDirect,
|
||||
/// Helius transaction subscription plus same-network HTTP hydration.
|
||||
HeliusTransactionHydrated,
|
||||
/// HTTP block polling and direct block material projection.
|
||||
HttpBlockPolling,
|
||||
}
|
||||
|
||||
impl crate::RawIngestRouteId {
|
||||
/// Returns all V1 route identifiers in stable UI order.
|
||||
#[must_use]
|
||||
pub(crate) fn all() -> std::vec::Vec<Self> {
|
||||
return vec![Self::YellowstoneHydrated, Self::StandardLogsHydrated, Self::StandardBlockDirect, Self::HeliusTransactionHydrated, Self::HttpBlockPolling];
|
||||
}
|
||||
|
||||
/// Returns the application-owned route family used by safe UI projections.
|
||||
#[must_use]
|
||||
pub(crate) const fn family(self) -> crate::RawIngestRouteFamily {
|
||||
return match self {
|
||||
Self::YellowstoneHydrated => crate::RawIngestRouteFamily::Yellowstone,
|
||||
Self::StandardLogsHydrated => crate::RawIngestRouteFamily::StandardLogs,
|
||||
Self::StandardBlockDirect => crate::RawIngestRouteFamily::StandardBlock,
|
||||
Self::HeliusTransactionHydrated => crate::RawIngestRouteFamily::HeliusTransaction,
|
||||
Self::HttpBlockPolling => crate::RawIngestRouteFamily::HttpBlockPolling,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the stable operator-facing route label without exposing physical endpoint metadata.
|
||||
#[must_use]
|
||||
pub(crate) const fn label(self) -> &'static str {
|
||||
return match self {
|
||||
Self::YellowstoneHydrated => "Yellowstone Block + HTTP block reconciliation",
|
||||
Self::StandardLogsHydrated => "Standard Logs + HTTP hydration",
|
||||
Self::StandardBlockDirect => "Standard Block direct",
|
||||
Self::HeliusTransactionHydrated => "Helius Transaction + HTTP hydration",
|
||||
Self::HttpBlockPolling => "HTTP Block Polling",
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the stable kebab-case route identifier used by safe logs and IPC.
|
||||
#[must_use]
|
||||
pub(crate) const fn as_str(self) -> &'static str {
|
||||
return match self {
|
||||
Self::YellowstoneHydrated => "yellowstone-hydrated",
|
||||
Self::StandardLogsHydrated => "standard-logs-hydrated",
|
||||
Self::StandardBlockDirect => "standard-block-direct",
|
||||
Self::HeliusTransactionHydrated => "helius-transaction-hydrated",
|
||||
Self::HttpBlockPolling => "http-block-polling",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Start-time commitments accepted by Raw Transaction Ingest Desk.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestCommitment.ts")]
|
||||
pub(crate) enum RawIngestCommitment {
|
||||
/// Confirmed commitment.
|
||||
Confirmed,
|
||||
/// Finalized commitment.
|
||||
Finalized,
|
||||
}
|
||||
|
||||
impl crate::RawIngestCommitment {
|
||||
/// Returns the stable wire label.
|
||||
#[must_use]
|
||||
pub(crate) const fn as_str(self) -> &'static str {
|
||||
return match self {
|
||||
Self::Confirmed => "confirmed",
|
||||
Self::Finalized => "finalized",
|
||||
};
|
||||
}
|
||||
|
||||
/// Maps the application-owned bounded commitment into the Transport contract.
|
||||
#[must_use]
|
||||
pub(crate) const fn into_transport(self) -> ksp_onchain_transport_lib::SolanaCommitment {
|
||||
return match self {
|
||||
Self::Confirmed => ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
|
||||
Self::Finalized => ksp_onchain_transport_lib::SolanaCommitment::Finalized,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Safe request used to revalidate and reconstruct one future route Start without accepting physical resources from the frontend.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStartRequestDto.ts")]
|
||||
pub(crate) struct RawIngestRouteStartRequestDto {
|
||||
/// Confirmed/finalized commitment selected for the future Worker.
|
||||
pub(crate) commitment: crate::RawIngestCommitment,
|
||||
/// Inventory generation observed by the caller.
|
||||
pub(crate) inventory_generation: u32,
|
||||
/// Safe logical network-profile identifier selected from the current inventory.
|
||||
pub(crate) profile_id: String,
|
||||
/// Stable logical route identifier.
|
||||
pub(crate) route_id: crate::RawIngestRouteId,
|
||||
}
|
||||
|
||||
/// Safe targeted Stop request containing only the logical Worker route identity.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStopRequestDto.ts")]
|
||||
pub(crate) struct RawIngestRouteStopRequestDto {
|
||||
/// Safe logical network-profile identifier that owns the Worker route.
|
||||
pub(crate) profile_id: String,
|
||||
/// Stable logical route identifier to stop.
|
||||
pub(crate) route_id: crate::RawIngestRouteId,
|
||||
}
|
||||
|
||||
const RAW_INGEST_PROFILE_ID_MAX_BYTES: usize = 256;
|
||||
|
||||
impl crate::RawIngestRouteStartRequestDto {
|
||||
/// Validates the only caller-controlled free-form logical identifier before Config or runtime reconstruction.
|
||||
pub(crate) fn validate_logical_identity(&self) -> ksp_core_lib::Result<()> {
|
||||
return validate_profile_id(self.profile_id.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::RawIngestRouteStopRequestDto {
|
||||
/// Validates the only caller-controlled free-form logical identifier before runtime lookup.
|
||||
pub(crate) fn validate_logical_identity(&self) -> ksp_core_lib::Result<()> {
|
||||
return validate_profile_id(self.profile_id.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_profile_id(profile_id: &str) -> ksp_core_lib::Result<()> {
|
||||
if profile_id.is_empty()
|
||||
|| profile_id.len() > RAW_INGEST_PROFILE_ID_MAX_BYTES
|
||||
|| profile_id.trim() != profile_id
|
||||
|| profile_id.chars().any(char::is_control)
|
||||
{
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_REQUEST_INVALID,
|
||||
"Raw Transaction Ingest Desk route request contains an invalid logical profile identifier",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
/// Safe acknowledgement that exact Start-time Transport resources were reconstructed and validated without Worker launch.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStartValidationDto.ts")]
|
||||
pub(crate) struct RawIngestRouteStartValidationDto {
|
||||
/// Commitment validated by the exact Worker source constructor.
|
||||
pub(crate) commitment: crate::RawIngestCommitment,
|
||||
/// Inventory generation validated against backend state.
|
||||
pub(crate) inventory_generation: u32,
|
||||
/// Logical network re-proven from Store and Transport Config.
|
||||
pub(crate) network: String,
|
||||
/// Safe logical network-profile identifier.
|
||||
pub(crate) profile_id: String,
|
||||
/// Exact logical route whose Transport resources were reconstructed.
|
||||
pub(crate) route_id: crate::RawIngestRouteId,
|
||||
}
|
||||
|
||||
/// Safe per-route runtime acknowledgement containing only logical identity and lifecycle state.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteRuntimeDto.ts")]
|
||||
pub(crate) struct RawIngestRouteRuntimeDto {
|
||||
/// Confirmed/finalized commitment bound to the active Worker.
|
||||
pub(crate) commitment: crate::RawIngestCommitment,
|
||||
/// Inventory generation revalidated before Store opening.
|
||||
pub(crate) inventory_generation: u32,
|
||||
/// Logical network bound identically to Store and Worker.
|
||||
pub(crate) network: String,
|
||||
/// Safe logical network-profile identifier.
|
||||
pub(crate) profile_id: String,
|
||||
/// Exact logical route owned by the independent Worker.
|
||||
pub(crate) route_id: crate::RawIngestRouteId,
|
||||
/// Safe application-owned projection of current Worker lifecycle.
|
||||
pub(crate) state: crate::RawIngestRouteState,
|
||||
}
|
||||
|
||||
/// Stable application-owned route families used only for safe presentation and orchestration.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteFamily.ts")]
|
||||
pub(crate) enum RawIngestRouteFamily {
|
||||
/// Yellowstone gRPC subscription route.
|
||||
Yellowstone,
|
||||
/// Standard Solana logs WebSocket route.
|
||||
StandardLogs,
|
||||
/// Standard Solana block WebSocket route.
|
||||
StandardBlock,
|
||||
/// Helius transaction WebSocket route.
|
||||
HeliusTransaction,
|
||||
/// HTTP block polling route.
|
||||
HttpBlockPolling,
|
||||
}
|
||||
|
||||
/// Safe reasons explaining why one logical route cannot currently be composed from Config.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteUnavailableReason.ts")]
|
||||
pub(crate) enum RawIngestRouteUnavailableReason {
|
||||
/// The selected composite or one of its typed Config components could not be resolved safely.
|
||||
ProfileUnresolved,
|
||||
/// Transport and Store do not prove one identical logical network.
|
||||
NetworkMismatch,
|
||||
/// No HTTP role can satisfy `getBlock` for same-network block reconciliation.
|
||||
MissingHttpGetBlock,
|
||||
/// No HTTP role can satisfy `getTransaction` for same-network hydration.
|
||||
MissingHttpGetTransaction,
|
||||
/// No HTTP role can satisfy the complete block-polling method set.
|
||||
MissingHttpBlockScan,
|
||||
/// No enabled standard WebSocket endpoint explicitly declares `Logs`.
|
||||
MissingWsLogsCapability,
|
||||
/// No enabled standard WebSocket endpoint explicitly declares `Block`.
|
||||
MissingWsBlockCapability,
|
||||
/// No enabled Helius WebSocket endpoint explicitly declares `HeliusTransaction`.
|
||||
MissingHeliusTransactionCapability,
|
||||
/// No enabled same-network Yellowstone gRPC endpoint is configured.
|
||||
MissingYellowstoneGrpc,
|
||||
/// A required Config secret is not resolved for the selected composite profile.
|
||||
MissingRequiredSecret,
|
||||
}
|
||||
|
||||
/// Stable Raw Transaction Ingest Desk route lifecycle states.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteState.ts")]
|
||||
pub(crate) enum RawIngestRouteState {
|
||||
/// Required Config composition is incomplete or incoherent.
|
||||
Unavailable,
|
||||
/// Config proves composability but no runtime Worker is active.
|
||||
Configured,
|
||||
/// Runtime resources are being revalidated and assembled.
|
||||
Starting,
|
||||
/// One Worker instance is active for the logical route.
|
||||
Running,
|
||||
/// Stop has been requested and bounded termination is pending.
|
||||
Stopping,
|
||||
/// The route Worker terminated normally and its handle was removed.
|
||||
Stopped,
|
||||
/// Start or runtime failed with a safe projected code.
|
||||
Faulted,
|
||||
}
|
||||
|
||||
impl crate::RawIngestRouteState {
|
||||
/// Returns all lifecycle states in stable presentation order.
|
||||
#[must_use]
|
||||
pub(crate) fn all() -> std::vec::Vec<Self> {
|
||||
return vec![Self::Unavailable, Self::Configured, Self::Starting, Self::Running, Self::Stopping, Self::Stopped, Self::Faulted];
|
||||
}
|
||||
}
|
||||
|
||||
/// Static route/state foundation retained as the bounded vocabulary behind the live Config inventory.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteFoundationDto.ts")]
|
||||
pub(crate) struct RawIngestRouteFoundationDto {
|
||||
/// Five stable V1 logical route identifiers; this is not a composability claim.
|
||||
pub(crate) route_ids: std::vec::Vec<crate::RawIngestRouteId>,
|
||||
/// Stable lifecycle state vocabulary reserved for runtime projections.
|
||||
pub(crate) states: std::vec::Vec<crate::RawIngestRouteState>,
|
||||
}
|
||||
|
||||
impl crate::RawIngestRouteFoundationDto {
|
||||
/// Builds the bounded route/state vocabulary without resolving endpoints, secrets or Worker resources.
|
||||
#[must_use]
|
||||
pub(crate) fn scaffold() -> Self {
|
||||
return Self { route_ids: crate::RawIngestRouteId::all(), states: crate::RawIngestRouteState::all() };
|
||||
}
|
||||
}
|
||||
|
||||
/// Safe Config-only composability projection for one logical route.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteDto.ts")]
|
||||
pub(crate) struct RawIngestRouteDto {
|
||||
/// Stable route family used for presentation and bounded orchestration.
|
||||
pub(crate) family: crate::RawIngestRouteFamily,
|
||||
/// Stable operator-facing label with no physical resource identity.
|
||||
pub(crate) label: String,
|
||||
/// Logical network proven by Config when available.
|
||||
pub(crate) network: std::option::Option<String>,
|
||||
/// Safe reason explaining an unavailable route.
|
||||
pub(crate) reason: std::option::Option<crate::RawIngestRouteUnavailableReason>,
|
||||
/// Stable logical V1 route identifier.
|
||||
pub(crate) route_id: crate::RawIngestRouteId,
|
||||
/// Whether all Config requirements for the route are currently composable.
|
||||
pub(crate) selectable: bool,
|
||||
/// Config-level lifecycle projection; `Configured` is not a runtime health claim.
|
||||
pub(crate) state: crate::RawIngestRouteState,
|
||||
}
|
||||
|
||||
impl crate::RawIngestRouteDto {
|
||||
/// Builds one route that is composable from Config without claiming runtime operability.
|
||||
#[must_use]
|
||||
pub(crate) fn configured(route_id: crate::RawIngestRouteId, network: &str) -> Self {
|
||||
return Self {
|
||||
family: route_id.family(),
|
||||
label: route_id.label().to_owned(),
|
||||
network: std::option::Option::Some(network.to_owned()),
|
||||
reason: std::option::Option::None,
|
||||
route_id,
|
||||
selectable: true,
|
||||
state: crate::RawIngestRouteState::Configured,
|
||||
};
|
||||
}
|
||||
|
||||
/// Builds one route that is not composable from the current Config profile.
|
||||
#[must_use]
|
||||
pub(crate) fn unavailable(route_id: crate::RawIngestRouteId, network: std::option::Option<&str>, reason: crate::RawIngestRouteUnavailableReason) -> Self {
|
||||
return Self {
|
||||
family: route_id.family(),
|
||||
label: route_id.label().to_owned(),
|
||||
network: network.map(str::to_owned),
|
||||
reason: std::option::Option::Some(reason),
|
||||
route_id,
|
||||
selectable: false,
|
||||
state: crate::RawIngestRouteState::Unavailable,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Safe route inventory for one logical Raw Transaction Ingest Desk composite profile.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestProfileInventoryDto.ts")]
|
||||
pub(crate) struct RawIngestProfileInventoryDto {
|
||||
/// Whether this profile is the composite's autonomous default.
|
||||
pub(crate) is_default: bool,
|
||||
/// Logical network proven from the resolved Config profile when available.
|
||||
pub(crate) network: std::option::Option<String>,
|
||||
/// Stable composite profile identifier.
|
||||
pub(crate) profile_id: String,
|
||||
/// Logical routes applicable to this network profile in stable V1 presentation order; provider-specific routes may be absent when their Config source is not declared.
|
||||
pub(crate) routes: std::vec::Vec<crate::RawIngestRouteDto>,
|
||||
}
|
||||
|
||||
/// Deterministic Config-only route inventory returned to the frontend.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteInventoryDto.ts")]
|
||||
pub(crate) struct RawIngestRouteInventoryDto {
|
||||
/// Composite default profile identifier.
|
||||
pub(crate) default_profile: String,
|
||||
/// Monotonic application-owned generation incremented after each successful inventory rebuild.
|
||||
pub(crate) generation: u32,
|
||||
/// Safe inventories for every validated composite profile in source order.
|
||||
pub(crate) profiles: std::vec::Vec<crate::RawIngestProfileInventoryDto>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/dto_route.rs"]
|
||||
mod tests;
|
||||
79
crates/ksp-app-raw-transaction-ingest-desk/src/errors.rs
Normal file
79
crates/ksp-app-raw-transaction-ingest-desk/src/errors.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/errors.rs
|
||||
// version: 6
|
||||
|
||||
//! Stable Raw Transaction Ingest Desk application error codes.
|
||||
|
||||
/// Shared Raw Transaction Ingest Desk application state is internally inconsistent.
|
||||
pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "app_state_invalid");
|
||||
/// Shared Raw Transaction Ingest Desk runtime state cannot be locked safely.
|
||||
pub(crate) const ERROR_CODE_APP_STATE_LOCK_FAILED: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "app_state_lock_failed");
|
||||
/// The Raw Transaction Ingest Desk Config composite is missing or misroutes a required component.
|
||||
pub(crate) const ERROR_CODE_CONFIG_COMPOSITE_INVALID: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "config_composite_invalid");
|
||||
/// Frontend logging requested an unsupported level.
|
||||
pub(crate) const ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "frontend_log_level_invalid");
|
||||
/// Frontend logging requested a target outside the application whitelist.
|
||||
pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "frontend_log_target_invalid");
|
||||
/// Frontend logging payload exceeds the bounded technical IPC contract.
|
||||
pub(crate) const ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "frontend_log_payload_invalid");
|
||||
/// Raw Transaction Ingest Desk could not install managed Logging or its safe fallback.
|
||||
pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "logging_bootstrap_failed");
|
||||
/// Config-only route inventory cannot be projected safely from the validated composite.
|
||||
pub(crate) const ERROR_CODE_ROUTE_INVENTORY_INVALID: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_inventory_invalid");
|
||||
/// Exact Start-time Transport resources cannot be reconstructed safely.
|
||||
pub(crate) const ERROR_CODE_ROUTE_START_PREPARATION_FAILED: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_start_preparation_failed");
|
||||
/// The selected logical route is already starting/active or shared Store lifecycle is temporarily exclusive.
|
||||
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_ACTIVE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_active");
|
||||
/// No selected route Worker runtime is currently active.
|
||||
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_NOT_ACTIVE: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_not_active");
|
||||
/// A selected route cannot share the currently open Store because its network differs.
|
||||
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_NETWORK_MISMATCH: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_network_mismatch");
|
||||
/// Store or Worker startup failed after Start-time route revalidation.
|
||||
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_START_FAILED: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_start_failed");
|
||||
/// The opened Store did not prove Ready before Worker launch.
|
||||
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_STORE_NOT_READY: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_store_not_ready");
|
||||
/// The Store could not be reclaimed or explicitly closed after Worker termination.
|
||||
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_store_shutdown_failed");
|
||||
/// The multi-route runtime reservation/handle state changed unexpectedly.
|
||||
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_STATE_INVALID: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_state_invalid");
|
||||
/// Application shutdown has closed admission for new route Worker starts.
|
||||
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_shutting_down");
|
||||
/// Application shutdown could not join all route Workers and shared Store cleanup within the bounded deadline.
|
||||
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_SHUTDOWN_FAILED: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_shutdown_failed");
|
||||
/// A Start/Stop IPC request contains an invalid logical identifier or unsupported shape.
|
||||
pub(crate) const ERROR_CODE_ROUTE_REQUEST_INVALID: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_request_invalid");
|
||||
/// Start request references an inventory generation that is no longer current.
|
||||
pub(crate) const ERROR_CODE_ROUTE_START_STALE_INVENTORY: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "stale_inventory");
|
||||
/// Start request references a route that is no longer Configured/applicable.
|
||||
pub(crate) const ERROR_CODE_ROUTE_START_UNAVAILABLE: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_start_unavailable");
|
||||
/// Splash readiness was invoked from a window other than the splash window.
|
||||
pub(crate) const ERROR_CODE_SPLASH_ORIGIN_INVALID: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "splash_origin_invalid");
|
||||
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
|
||||
pub(crate) const ERROR_CODE_SPLASH_SETTING_INVALID: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "splash_setting_invalid");
|
||||
/// Tauri runtime assembly or execution failed.
|
||||
pub(crate) const ERROR_CODE_TAURI_RUNTIME_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "tauri_runtime_failed");
|
||||
/// A required Tauri window is missing from the configured application runtime.
|
||||
pub(crate) const ERROR_CODE_TAURI_WINDOW_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "tauri_window_missing");
|
||||
/// A Tauri window show/focus/destroy/event operation failed.
|
||||
pub(crate) const ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "tauri_window_operation_failed");
|
||||
@@ -0,0 +1,241 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/frontend_logging.rs
|
||||
// version: 2
|
||||
|
||||
//! KSP-owned bridge for technical log events emitted by Raw Transaction Ingest Desk frontend scripts.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Log payload sent by Raw Transaction Ingest Desk frontend scripts.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/frontend_logging/FrontendLogPayloadDto.ts")]
|
||||
pub(crate) struct FrontendLogPayloadDto {
|
||||
/// Lowercase KSP log level.
|
||||
pub(crate) level: String,
|
||||
/// Whitelisted logical frontend target identifier.
|
||||
pub(crate) target_id: String,
|
||||
/// Rendered technical frontend message.
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum FrontendLogLevel {
|
||||
Trace,
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum FrontendLogTarget {
|
||||
Frontend,
|
||||
Main,
|
||||
Splash,
|
||||
}
|
||||
|
||||
const FRONTEND_LOG_MESSAGE_MAX_BYTES: usize = 8192;
|
||||
const FRONTEND_LOG_SELECTOR_MAX_BYTES: usize = 16;
|
||||
|
||||
/// Emits one validated frontend event through the KSP Logging facade.
|
||||
pub(crate) fn emit_frontend_log_event(payload: FrontendLogPayloadDto) -> ksp_core_lib::Result<()> {
|
||||
let shape = validate_payload_shape(&payload);
|
||||
if let std::result::Result::Err(error) = shape {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let level = parse_level(payload.level.as_str());
|
||||
let level = match level {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let target = parse_target(payload.target_id.as_str());
|
||||
let target = match target {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
emit_validated_frontend_log(level, target, payload.message.as_str());
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn validate_payload_shape(payload: &FrontendLogPayloadDto) -> ksp_core_lib::Result<()> {
|
||||
if payload.level.len() > FRONTEND_LOG_SELECTOR_MAX_BYTES
|
||||
|| payload.target_id.len() > FRONTEND_LOG_SELECTOR_MAX_BYTES
|
||||
|| payload.message.len() > FRONTEND_LOG_MESSAGE_MAX_BYTES
|
||||
|| payload.message.chars().any(char::is_control)
|
||||
{
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID,
|
||||
"Frontend log payload exceeds the bounded technical IPC contract",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn parse_level(level: &str) -> ksp_core_lib::Result<FrontendLogLevel> {
|
||||
let level = level.trim();
|
||||
if level.eq_ignore_ascii_case("trace") {
|
||||
return std::result::Result::Ok(FrontendLogLevel::Trace);
|
||||
}
|
||||
if level.eq_ignore_ascii_case("debug") {
|
||||
return std::result::Result::Ok(FrontendLogLevel::Debug);
|
||||
}
|
||||
if level.eq_ignore_ascii_case("info") {
|
||||
return std::result::Result::Ok(FrontendLogLevel::Info);
|
||||
}
|
||||
if level.eq_ignore_ascii_case("warn") {
|
||||
return std::result::Result::Ok(FrontendLogLevel::Warn);
|
||||
}
|
||||
if level.eq_ignore_ascii_case("error") {
|
||||
return std::result::Result::Ok(FrontendLogLevel::Error);
|
||||
}
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID, "Frontend log level is not supported"));
|
||||
}
|
||||
|
||||
fn parse_target(target_id: &str) -> ksp_core_lib::Result<FrontendLogTarget> {
|
||||
let target_id = target_id.trim();
|
||||
if target_id.eq_ignore_ascii_case("frontend") {
|
||||
return std::result::Result::Ok(FrontendLogTarget::Frontend);
|
||||
}
|
||||
if target_id.eq_ignore_ascii_case("main") {
|
||||
return std::result::Result::Ok(FrontendLogTarget::Main);
|
||||
}
|
||||
if target_id.eq_ignore_ascii_case("splash") {
|
||||
return std::result::Result::Ok(FrontendLogTarget::Splash);
|
||||
}
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID,
|
||||
"Frontend log target identifier is not supported",
|
||||
));
|
||||
}
|
||||
|
||||
fn emit_validated_frontend_log(level: FrontendLogLevel, target: FrontendLogTarget, message: &str) {
|
||||
return match target {
|
||||
FrontendLogTarget::Frontend => emit_frontend_target(level, message),
|
||||
FrontendLogTarget::Main => emit_main_target(level, message),
|
||||
FrontendLogTarget::Splash => emit_splash_target(level, message),
|
||||
};
|
||||
}
|
||||
|
||||
fn emit_frontend_target(level: FrontendLogLevel, message: &str) {
|
||||
return match level {
|
||||
FrontendLogLevel::Trace => ksp_logging_lib::trace!(
|
||||
target: crate::TRACING_TARGET_FRONTEND,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "frontend",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Debug => ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET_FRONTEND,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "frontend",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Info => ksp_logging_lib::info!(
|
||||
target: crate::TRACING_TARGET_FRONTEND,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "frontend",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Warn => ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET_FRONTEND,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "frontend",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Error => ksp_logging_lib::error!(
|
||||
target: crate::TRACING_TARGET_FRONTEND,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "frontend",
|
||||
"{message}"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn emit_main_target(level: FrontendLogLevel, message: &str) {
|
||||
return match level {
|
||||
FrontendLogLevel::Trace => ksp_logging_lib::trace!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_MAIN,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "main",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Debug => ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_MAIN,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "main",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Info => ksp_logging_lib::info!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_MAIN,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "main",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Warn => ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_MAIN,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "main",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Error => ksp_logging_lib::error!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_MAIN,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "main",
|
||||
"{message}"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn emit_splash_target(level: FrontendLogLevel, message: &str) {
|
||||
return match level {
|
||||
FrontendLogLevel::Trace => ksp_logging_lib::trace!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_SPLASH,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "splash",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Debug => ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_SPLASH,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "splash",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Info => ksp_logging_lib::info!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_SPLASH,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "splash",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Warn => ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_SPLASH,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "splash",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Error => ksp_logging_lib::error!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_SPLASH,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "splash",
|
||||
"{message}"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/frontend_logging.rs"]
|
||||
mod tests;
|
||||
201
crates/ksp-app-raw-transaction-ingest-desk/src/lib.rs
Normal file
201
crates/ksp-app-raw-transaction-ingest-desk/src/lib.rs
Normal file
@@ -0,0 +1,201 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/lib.rs
|
||||
// version: 8
|
||||
|
||||
//! Tauri desktop application scaffold for composing and supervising KSP live RAW transaction ingest routes.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
mod app_state;
|
||||
mod bootstrap;
|
||||
mod constants;
|
||||
mod dto_common;
|
||||
mod dto_route;
|
||||
mod errors;
|
||||
mod frontend_logging;
|
||||
mod logging_runtime;
|
||||
mod route_inventory;
|
||||
mod route_monitoring;
|
||||
mod route_runtime;
|
||||
mod route_start;
|
||||
mod splash;
|
||||
mod tauri;
|
||||
mod tw_main;
|
||||
mod tw_splash;
|
||||
|
||||
/// Runs the KSP Raw Transaction Ingest desktop application.
|
||||
pub use self::tauri::run;
|
||||
|
||||
/// Shared Raw Transaction Ingest Desk application state managed by Tauri.
|
||||
pub(crate) use self::app_state::AppState;
|
||||
/// Crate-internal Logging startup state shared by the application state.
|
||||
pub(crate) use self::bootstrap::LoggingStartup;
|
||||
/// Builds the Config management facade from the common KSP CLI bootstrap contract.
|
||||
pub(crate) use self::bootstrap::config_management;
|
||||
/// Initializes Logging from the Raw Transaction Ingest Desk composite with a bounded fallback.
|
||||
pub(crate) use self::bootstrap::initialize_logging;
|
||||
/// Loads the Raw Transaction Ingest Desk composite selected by its registered logical file identifier.
|
||||
pub(crate) use self::bootstrap::load_raw_transaction_ingest_desk_composite;
|
||||
/// Loads one explicit or default Raw Transaction Ingest Desk composite profile.
|
||||
pub(crate) use self::bootstrap::load_raw_transaction_ingest_desk_composite_profile;
|
||||
/// Resolves one required standard Config profile from the Raw Transaction Ingest Desk composite.
|
||||
pub(crate) use self::bootstrap::required_composite_component_profile;
|
||||
/// Composite-local identifier for the standard Logging component.
|
||||
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_LOGGING;
|
||||
/// Composite-local identifier for the standard Store component.
|
||||
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_STORE;
|
||||
/// Composite-local identifier for the standard Transport component.
|
||||
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT;
|
||||
/// Composite-local optional Transport source for Helius WebSocket capabilities.
|
||||
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT_HELIUS;
|
||||
/// Prefix used by optional same-network Transport capability sources.
|
||||
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT_PREFIX;
|
||||
/// Composite-local optional Transport source for Yellowstone gRPC capabilities.
|
||||
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT_YELLOWSTONE;
|
||||
/// Structured domain used while bootstrapping Config and Logging.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP;
|
||||
/// Structured domain used by technical frontend events.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
|
||||
/// Structured domain used while rebuilding Config-only route inventory.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_INVENTORY;
|
||||
/// Structured domain used by multi-route shared-Store and Worker lifecycle operations.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_ROUTE_RUNTIME;
|
||||
/// Structured domain used while revalidating future Start requests.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_ROUTE_START;
|
||||
/// Structured domain used by the Raw Transaction Ingest Desk shell.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
|
||||
/// Structured domain used by Tauri window lifecycle operations.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
|
||||
/// Owning target for backend events emitted by Raw Transaction Ingest Desk.
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
/// Owning target for generic frontend events emitted through the KSP bridge.
|
||||
pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
|
||||
/// Owning target for main-window frontend events.
|
||||
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN;
|
||||
/// Owning target for splash-window frontend events.
|
||||
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
|
||||
/// Safe command error projection exposed to Tauri commands.
|
||||
pub(crate) use self::dto_common::CommandErrorDto;
|
||||
/// Safe scaffold/runtime snapshot exposed to the shell.
|
||||
pub(crate) use self::dto_common::ShellStatusDto;
|
||||
/// Bounded confirmed/finalized commitment accepted by future route Start requests.
|
||||
pub(crate) use self::dto_route::RawIngestCommitment;
|
||||
/// Safe route inventory for one logical composite profile.
|
||||
pub(crate) use self::dto_route::RawIngestProfileInventoryDto;
|
||||
/// Safe Config-only composability projection for one logical route.
|
||||
pub(crate) use self::dto_route::RawIngestRouteDto;
|
||||
/// Stable application-owned route family.
|
||||
pub(crate) use self::dto_route::RawIngestRouteFamily;
|
||||
/// Static route/state foundation retained as the bounded vocabulary behind the Config inventory.
|
||||
pub(crate) use self::dto_route::RawIngestRouteFoundationDto;
|
||||
/// Stable logical V1 route identifiers owned by the Desk.
|
||||
pub(crate) use self::dto_route::RawIngestRouteId;
|
||||
/// Complete deterministic Config-only route inventory.
|
||||
pub(crate) use self::dto_route::RawIngestRouteInventoryDto;
|
||||
/// Safe per-route runtime acknowledgement containing only logical identity and lifecycle.
|
||||
pub(crate) use self::dto_route::RawIngestRouteRuntimeDto;
|
||||
/// Safe future Start request containing only logical route selection and inventory generation.
|
||||
pub(crate) use self::dto_route::RawIngestRouteStartRequestDto;
|
||||
/// Safe acknowledgement that exact Start-time resources were reconstructed without Worker launch.
|
||||
pub(crate) use self::dto_route::RawIngestRouteStartValidationDto;
|
||||
/// Stable route lifecycle states reserved for runtime projections.
|
||||
pub(crate) use self::dto_route::RawIngestRouteState;
|
||||
/// Safe targeted Stop request containing only logical route identity.
|
||||
pub(crate) use self::dto_route::RawIngestRouteStopRequestDto;
|
||||
/// Safe reason explaining why one logical route is not composable from Config.
|
||||
pub(crate) use self::dto_route::RawIngestRouteUnavailableReason;
|
||||
/// Shared Raw Transaction Ingest Desk application state is internally inconsistent.
|
||||
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
|
||||
/// Shared Raw Transaction Ingest Desk runtime state cannot be locked safely.
|
||||
pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
|
||||
/// Raw Transaction Ingest Desk Config composite is missing or invalid.
|
||||
pub(crate) use self::errors::ERROR_CODE_CONFIG_COMPOSITE_INVALID;
|
||||
/// Frontend logging requested an unsupported level.
|
||||
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
|
||||
/// Frontend logging payload exceeds the bounded technical IPC contract.
|
||||
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID;
|
||||
/// Frontend logging requested an unsupported target.
|
||||
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
|
||||
/// Managed and fallback Logging initialization failed.
|
||||
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
|
||||
/// Config-only route inventory cannot be projected safely.
|
||||
pub(crate) use self::errors::ERROR_CODE_ROUTE_INVENTORY_INVALID;
|
||||
/// Start/Stop IPC request contains an invalid logical shape.
|
||||
pub(crate) use self::errors::ERROR_CODE_ROUTE_REQUEST_INVALID;
|
||||
/// A Worker runtime already owns the same logical route in the shared application runtime.
|
||||
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_ACTIVE;
|
||||
/// Selected route cannot share the currently open Store because its network differs.
|
||||
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_NETWORK_MISMATCH;
|
||||
/// No selected route Worker runtime is currently active.
|
||||
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_NOT_ACTIVE;
|
||||
/// Application shutdown could not complete all bounded route/Store cleanup.
|
||||
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_SHUTDOWN_FAILED;
|
||||
/// Application shutdown has closed admission for new route Worker starts.
|
||||
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN;
|
||||
/// Store or Worker startup failed after Start-time route revalidation.
|
||||
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_START_FAILED;
|
||||
/// The multi-route runtime reservation or handle state changed unexpectedly.
|
||||
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_STATE_INVALID;
|
||||
/// The opened Store did not prove Ready before Worker launch.
|
||||
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_STORE_NOT_READY;
|
||||
/// The Store could not be reclaimed or explicitly closed after Worker termination.
|
||||
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED;
|
||||
/// Exact Start-time Transport resources cannot be reconstructed safely.
|
||||
pub(crate) use self::errors::ERROR_CODE_ROUTE_START_PREPARATION_FAILED;
|
||||
/// Start request references a stale inventory generation.
|
||||
pub(crate) use self::errors::ERROR_CODE_ROUTE_START_STALE_INVENTORY;
|
||||
/// Start request references a route that is no longer Configured/applicable.
|
||||
pub(crate) use self::errors::ERROR_CODE_ROUTE_START_UNAVAILABLE;
|
||||
/// Splash readiness originated from an invalid window.
|
||||
pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
|
||||
/// Splash timing configuration is invalid.
|
||||
pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID;
|
||||
/// Tauri runtime assembly or execution failed.
|
||||
pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED;
|
||||
/// A required Tauri window is missing.
|
||||
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING;
|
||||
/// A Tauri window operation failed.
|
||||
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED;
|
||||
/// Log payload sent by Raw Transaction Ingest Desk frontend scripts.
|
||||
pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
|
||||
/// Emits one validated frontend event through the KSP Logging facade.
|
||||
pub(crate) use self::frontend_logging::emit_frontend_log_event;
|
||||
/// Creates the stable runtime identity for this Raw Transaction Ingest Desk process launch.
|
||||
pub(crate) use self::logging_runtime::launch_identity;
|
||||
/// Rebuilds the complete safe route inventory from validated Config without runtime I/O.
|
||||
pub(crate) use self::route_inventory::build_route_inventory;
|
||||
/// Rebuilds the route inventory from one already captured Config environment snapshot.
|
||||
pub(crate) use self::route_inventory::build_route_inventory_with_environment;
|
||||
/// Stable Tauri event carrying one complete latest-value route monitoring projection.
|
||||
pub(crate) use self::route_monitoring::RAW_INGEST_ROUTE_STATUS_EVENT_NAME;
|
||||
/// Frontend-safe bounded projection of one run-local continuity gap.
|
||||
pub(crate) use self::route_monitoring::RawIngestRouteGapDto;
|
||||
/// Frontend-safe latest-value monitoring projection of one independent route Worker.
|
||||
pub(crate) use self::route_monitoring::RawIngestRouteMonitoringDto;
|
||||
/// Projects one complete concrete Worker snapshot to the safe route monitoring contract.
|
||||
pub(crate) use self::route_monitoring::project_route_monitoring;
|
||||
/// Launch ownership for one independent route Worker sharing the application Store when eligible.
|
||||
pub(crate) use self::route_runtime::RouteRuntimeLaunch;
|
||||
/// Shared multi-route runtime state with one independent Worker per route and one same-network Store.
|
||||
pub(crate) use self::route_runtime::RouteRuntimeState;
|
||||
/// Opens Store and starts one exact route Worker after Start-time revalidation.
|
||||
pub(crate) use self::route_runtime::start_route_runtime;
|
||||
/// Fully revalidated Start preparation retaining Store settings and exact Worker resources.
|
||||
pub(crate) use self::route_start::PreparedRouteStart;
|
||||
/// Revalidates one Start request and retains its backend-only Store/Worker resources.
|
||||
pub(crate) use self::route_start::prepare_route_start;
|
||||
/// Revalidates one future Start request and reconstructs its exact Transport-owned Worker source contract without launch.
|
||||
pub(crate) use self::route_start::validate_route_start;
|
||||
/// Command emitted by Rust to the splash frontend.
|
||||
pub(crate) use self::splash::SplashOrderDto;
|
||||
/// Runtime timings used by the common desk splash lifecycle.
|
||||
pub(crate) use self::splash::SplashSettings;
|
||||
/// Resolves the required main window or returns a typed error.
|
||||
pub(crate) use self::tw_main::require_main_window;
|
||||
/// Shows and focuses the main Raw Transaction Ingest Desk window.
|
||||
pub(crate) use self::tw_main::show_main_window;
|
||||
/// Resolves the required splash window or returns a typed error.
|
||||
pub(crate) use self::tw_splash::require_splash_window;
|
||||
/// Starts the one-shot splash lifecycle after the splash frontend reports readiness.
|
||||
pub(crate) use self::tw_splash::splash_frontend_ready_service;
|
||||
@@ -0,0 +1,10 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/logging_runtime.rs
|
||||
// version: 1
|
||||
|
||||
//! Raw Transaction Ingest Desk Logging runtime identity helpers.
|
||||
|
||||
/// Creates the stable runtime identity for this Raw Transaction Ingest Desk process launch.
|
||||
pub(crate) fn launch_identity() -> ksp_core_lib::Result<ksp_logging_lib::LoggingRuntimeIdentity> {
|
||||
let timestamp = format!("{}-p{}", chrono::Utc::now().format("%Y%m%d-%H%M%S%.3fZ"), std::process::id());
|
||||
return ksp_logging_lib::LoggingRuntimeIdentity::new(crate::TRACING_TARGET, timestamp);
|
||||
}
|
||||
58
crates/ksp-app-raw-transaction-ingest-desk/src/main.rs
Normal file
58
crates/ksp-app-raw-transaction-ingest-desk/src/main.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/main.rs
|
||||
// version: 1
|
||||
|
||||
//! Binary entry point for the KSP Raw Transaction Ingest desktop application.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![warn(missing_docs)]
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use fs2::FileExt; // rust-rules: trait-import
|
||||
|
||||
fn main() -> std::process::ExitCode {
|
||||
let working_directory = configure_runtime_working_directory();
|
||||
if let std::result::Result::Err(error) = working_directory {
|
||||
eprintln!("cannot configure Raw Transaction Ingest Desk runtime working directory: {error}");
|
||||
return std::process::ExitCode::FAILURE;
|
||||
}
|
||||
let mut lock_path = std::env::temp_dir();
|
||||
lock_path.push("com_sasedev_ksp_app_raw_transaction_ingest_desk.lock");
|
||||
let lock_file = match std::fs::OpenOptions::new().read(true).write(true).create(true).truncate(false).open(&lock_path) {
|
||||
std::result::Result::Ok(file) => file,
|
||||
std::result::Result::Err(error) => {
|
||||
eprintln!("cannot create application lock '{}': {error}", lock_path.display());
|
||||
return std::process::ExitCode::FAILURE;
|
||||
},
|
||||
};
|
||||
if let std::result::Result::Err(error) = lock_file.try_lock_exclusive() {
|
||||
if error.kind() == std::io::ErrorKind::WouldBlock {
|
||||
eprintln!("another ksp-app-raw-transaction-ingest-desk instance is already running");
|
||||
return std::process::ExitCode::FAILURE;
|
||||
}
|
||||
eprintln!("cannot acquire application lock '{}': {error}", lock_path.display());
|
||||
return std::process::ExitCode::FAILURE;
|
||||
}
|
||||
let _lock_file = lock_file;
|
||||
let arguments = std::env::args_os().collect::<std::vec::Vec<std::ffi::OsString>>();
|
||||
let run_result = ksp_app_raw_transaction_ingest_desk_lib::run(arguments.as_slice());
|
||||
return match run_result {
|
||||
std::result::Result::Ok(()) => std::process::ExitCode::SUCCESS,
|
||||
std::result::Result::Err(error) => {
|
||||
eprintln!("application error: {error}");
|
||||
std::process::ExitCode::FAILURE
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn configure_runtime_working_directory() -> std::io::Result<()> {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
return std::env::set_current_dir(workspace_root);
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_inventory.rs
|
||||
// version: 6
|
||||
|
||||
//! Config-only logical route inventory for Raw Transaction Ingest Desk.
|
||||
|
||||
struct CompositeProfileCatalog {
|
||||
default_profile: String,
|
||||
profile_ids: std::vec::Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct RouteCapabilities {
|
||||
has_get_transaction: bool,
|
||||
has_http_block_scan: bool,
|
||||
has_yellowstone: bool,
|
||||
has_standard_logs: bool,
|
||||
has_standard_block: bool,
|
||||
has_helius_transaction: bool,
|
||||
helius_source_declared: bool,
|
||||
helius_source_missing_secret: bool,
|
||||
yellowstone_has_get_block: bool,
|
||||
yellowstone_source_missing_secret: bool,
|
||||
}
|
||||
|
||||
impl RouteCapabilities {
|
||||
fn absorb_transport(&mut self, transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) {
|
||||
self.has_get_transaction |= http_role_supports_methods(transport.http_settings(), &["getTransaction"]);
|
||||
self.has_http_block_scan |= http_role_supports_methods(transport.http_settings(), &["getSlot", "getBlocksWithLimit", "getBlock"]);
|
||||
self.has_yellowstone |= has_yellowstone_grpc(transport, network);
|
||||
self.has_standard_logs |= has_ws_capability(
|
||||
transport,
|
||||
network,
|
||||
ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard,
|
||||
ksp_onchain_transport_lib::WsSubscriptionKind::Logs,
|
||||
);
|
||||
self.has_standard_block |= has_ws_capability(
|
||||
transport,
|
||||
network,
|
||||
ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard,
|
||||
ksp_onchain_transport_lib::WsSubscriptionKind::Block,
|
||||
);
|
||||
self.has_helius_transaction |= has_ws_capability(
|
||||
transport,
|
||||
network,
|
||||
ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream,
|
||||
ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuilds the complete safe route inventory from validated Config without opening any network, Store or Worker resource.
|
||||
pub(crate) fn build_route_inventory(management: &ksp_config_lib::ConfigManagement, generation: u32) -> ksp_core_lib::Result<crate::RawIngestRouteInventoryDto> {
|
||||
let environment = ksp_config_lib::ConfigEnvironment::load();
|
||||
let environment = match environment {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return build_route_inventory_with_environment(management, &environment, generation);
|
||||
}
|
||||
|
||||
/// Rebuilds the complete safe route inventory from one already captured Config environment snapshot.
|
||||
pub(crate) fn build_route_inventory_with_environment(
|
||||
management: &ksp_config_lib::ConfigManagement,
|
||||
environment: &ksp_config_lib::ConfigEnvironment,
|
||||
generation: u32,
|
||||
) -> ksp_core_lib::Result<crate::RawIngestRouteInventoryDto> {
|
||||
let catalog = composite_profile_catalog(management);
|
||||
let catalog = match catalog {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut profiles = std::vec::Vec::with_capacity(catalog.profile_ids.len());
|
||||
for profile_id in &catalog.profile_ids {
|
||||
profiles.push(build_profile_inventory(management, environment, profile_id.as_str(), profile_id == &catalog.default_profile));
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_INVENTORY,
|
||||
generation,
|
||||
profile_count = profiles.len(),
|
||||
"rebuilt Raw Transaction Ingest Desk network-centric Config-only route inventory"
|
||||
);
|
||||
return std::result::Result::Ok(crate::RawIngestRouteInventoryDto { default_profile: catalog.default_profile, generation, profiles });
|
||||
}
|
||||
|
||||
fn composite_profile_catalog(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<CompositeProfileCatalog> {
|
||||
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK);
|
||||
let file_id = match file_id {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let document = management.engine().load_validated_document(&file_id);
|
||||
let document = match document {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let root = document.value().as_object();
|
||||
let root = match root {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return inventory_invalid("validated Raw Transaction Ingest Desk composite root is not an object"),
|
||||
};
|
||||
let default_profile = root.get("default_profile").and_then(|value| return value.as_str());
|
||||
let default_profile = match default_profile {
|
||||
std::option::Option::Some(value) if !value.is_empty() => value.to_owned(),
|
||||
_ => return inventory_invalid("validated Raw Transaction Ingest Desk composite has no readable default profile"),
|
||||
};
|
||||
let profile_values = root.get("profiles").and_then(|value| return value.as_array());
|
||||
let profile_values = match profile_values {
|
||||
std::option::Option::Some(value) if !value.is_empty() => value,
|
||||
_ => return inventory_invalid("validated Raw Transaction Ingest Desk composite has no profile inventory"),
|
||||
};
|
||||
let mut profile_ids = std::vec::Vec::with_capacity(profile_values.len());
|
||||
let mut seen = std::collections::BTreeSet::<String>::new();
|
||||
for profile in profile_values {
|
||||
let profile_id = profile.get("profile_id").and_then(|value| return value.as_str());
|
||||
let profile_id = match profile_id {
|
||||
std::option::Option::Some(value) if !value.is_empty() => value,
|
||||
_ => return inventory_invalid("validated Raw Transaction Ingest Desk composite contains an unreadable profile identifier"),
|
||||
};
|
||||
if !seen.insert(profile_id.to_owned()) {
|
||||
return inventory_invalid("validated Raw Transaction Ingest Desk composite contains duplicate profile identifiers");
|
||||
}
|
||||
profile_ids.push(profile_id.to_owned());
|
||||
}
|
||||
if !seen.contains(default_profile.as_str()) {
|
||||
return inventory_invalid("validated Raw Transaction Ingest Desk composite default profile is absent from the profile inventory");
|
||||
}
|
||||
return std::result::Result::Ok(CompositeProfileCatalog { default_profile, profile_ids });
|
||||
}
|
||||
|
||||
fn build_profile_inventory(
|
||||
management: &ksp_config_lib::ConfigManagement,
|
||||
environment: &ksp_config_lib::ConfigEnvironment,
|
||||
profile_id: &str,
|
||||
is_default: bool,
|
||||
) -> crate::RawIngestProfileInventoryDto {
|
||||
let composite = crate::load_raw_transaction_ingest_desk_composite_profile(management, std::option::Option::Some(profile_id));
|
||||
let composite = match composite {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return unavailable_profile(profile_id, is_default, std::option::Option::None, crate::RawIngestRouteUnavailableReason::ProfileUnresolved);
|
||||
},
|
||||
};
|
||||
let store_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_STORE, ksp_config_lib::FILE_ID_STD_STORE);
|
||||
let store_profile = match store_profile {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return unavailable_profile(profile_id, is_default, std::option::Option::None, crate::RawIngestRouteUnavailableReason::ProfileUnresolved);
|
||||
},
|
||||
};
|
||||
let store = management.engine().resolve_store_config_profile(&store_profile, environment);
|
||||
let store = match store {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
let reason = resolution_error_reason(&error);
|
||||
return unavailable_profile(profile_id, is_default, std::option::Option::None, reason);
|
||||
},
|
||||
};
|
||||
let network = store.settings().network().as_str().to_owned();
|
||||
let base_transport_profile =
|
||||
crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT, ksp_config_lib::FILE_ID_STD_TRANSPORT);
|
||||
let base_transport_profile = match base_transport_profile {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return unavailable_profile(
|
||||
profile_id,
|
||||
is_default,
|
||||
std::option::Option::Some(network.as_str()),
|
||||
crate::RawIngestRouteUnavailableReason::ProfileUnresolved,
|
||||
);
|
||||
},
|
||||
};
|
||||
let base_transport = management.engine().resolve_transport_config_profile(&base_transport_profile, environment);
|
||||
let base_transport = match base_transport {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return unavailable_profile(profile_id, is_default, std::option::Option::Some(network.as_str()), resolution_error_reason(&error));
|
||||
},
|
||||
};
|
||||
if !transport_matches_network(&base_transport, network.as_str()) {
|
||||
return unavailable_profile(
|
||||
profile_id,
|
||||
is_default,
|
||||
std::option::Option::Some(network.as_str()),
|
||||
crate::RawIngestRouteUnavailableReason::NetworkMismatch,
|
||||
);
|
||||
}
|
||||
let mut capabilities = RouteCapabilities::default();
|
||||
capabilities.absorb_transport(&base_transport, network.as_str());
|
||||
for (component_id, component) in composite.components() {
|
||||
if component_id == crate::COMPOSITE_COMPONENT_ID_TRANSPORT {
|
||||
continue;
|
||||
}
|
||||
if !component_id.starts_with(crate::COMPOSITE_COMPONENT_ID_TRANSPORT_PREFIX) {
|
||||
continue;
|
||||
}
|
||||
if component_id != crate::COMPOSITE_COMPONENT_ID_TRANSPORT_HELIUS && component_id != crate::COMPOSITE_COMPONENT_ID_TRANSPORT_YELLOWSTONE {
|
||||
return unavailable_profile(
|
||||
profile_id,
|
||||
is_default,
|
||||
std::option::Option::Some(network.as_str()),
|
||||
crate::RawIngestRouteUnavailableReason::ProfileUnresolved,
|
||||
);
|
||||
}
|
||||
if component_id == crate::COMPOSITE_COMPONENT_ID_TRANSPORT_HELIUS {
|
||||
capabilities.helius_source_declared = true;
|
||||
}
|
||||
if component.resolved().file_id().as_str() != ksp_config_lib::FILE_ID_STD_TRANSPORT {
|
||||
return unavailable_profile(
|
||||
profile_id,
|
||||
is_default,
|
||||
std::option::Option::Some(network.as_str()),
|
||||
crate::RawIngestRouteUnavailableReason::ProfileUnresolved,
|
||||
);
|
||||
}
|
||||
let resolved = management.engine().resolve_transport_config_profile(component.resolved(), environment);
|
||||
let resolved = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) if error.code() == ksp_config_lib::ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING => {
|
||||
mark_optional_source_missing_secret(&mut capabilities, component_id.as_str());
|
||||
continue;
|
||||
},
|
||||
std::result::Result::Err(_) => {
|
||||
return unavailable_profile(
|
||||
profile_id,
|
||||
is_default,
|
||||
std::option::Option::Some(network.as_str()),
|
||||
crate::RawIngestRouteUnavailableReason::ProfileUnresolved,
|
||||
);
|
||||
},
|
||||
};
|
||||
if !transport_matches_network(&resolved, network.as_str()) {
|
||||
return unavailable_profile(
|
||||
profile_id,
|
||||
is_default,
|
||||
std::option::Option::Some(network.as_str()),
|
||||
crate::RawIngestRouteUnavailableReason::NetworkMismatch,
|
||||
);
|
||||
}
|
||||
if component_id == crate::COMPOSITE_COMPONENT_ID_TRANSPORT_YELLOWSTONE {
|
||||
capabilities.yellowstone_has_get_block = http_role_supports_methods(resolved.http_settings(), &["getBlock"]);
|
||||
}
|
||||
capabilities.absorb_transport(&resolved, network.as_str());
|
||||
}
|
||||
let routes = build_configured_profile_routes(&capabilities, network.as_str());
|
||||
return crate::RawIngestProfileInventoryDto { is_default, network: std::option::Option::Some(network), profile_id: profile_id.to_owned(), routes };
|
||||
}
|
||||
|
||||
fn mark_optional_source_missing_secret(capabilities: &mut RouteCapabilities, component_id: &str) {
|
||||
if component_id == crate::COMPOSITE_COMPONENT_ID_TRANSPORT_HELIUS {
|
||||
capabilities.helius_source_missing_secret = true;
|
||||
}
|
||||
if component_id == crate::COMPOSITE_COMPONENT_ID_TRANSPORT_YELLOWSTONE {
|
||||
capabilities.yellowstone_source_missing_secret = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn build_configured_profile_routes(capabilities: &RouteCapabilities, network: &str) -> std::vec::Vec<crate::RawIngestRouteDto> {
|
||||
let yellowstone_reason = if capabilities.yellowstone_source_missing_secret {
|
||||
crate::RawIngestRouteUnavailableReason::MissingRequiredSecret
|
||||
} else {
|
||||
crate::RawIngestRouteUnavailableReason::MissingYellowstoneGrpc
|
||||
};
|
||||
let helius_reason = if capabilities.helius_source_missing_secret {
|
||||
crate::RawIngestRouteUnavailableReason::MissingRequiredSecret
|
||||
} else {
|
||||
crate::RawIngestRouteUnavailableReason::MissingHeliusTransactionCapability
|
||||
};
|
||||
let mut routes = vec![
|
||||
route_with_requirements(
|
||||
crate::RawIngestRouteId::YellowstoneHydrated,
|
||||
network,
|
||||
&[
|
||||
(capabilities.has_yellowstone, yellowstone_reason),
|
||||
(capabilities.yellowstone_has_get_block, crate::RawIngestRouteUnavailableReason::MissingHttpGetBlock),
|
||||
],
|
||||
),
|
||||
route_with_requirements(
|
||||
crate::RawIngestRouteId::StandardLogsHydrated,
|
||||
network,
|
||||
&[
|
||||
(capabilities.has_standard_logs, crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability),
|
||||
(capabilities.has_get_transaction, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction),
|
||||
],
|
||||
),
|
||||
route_with_requirements(
|
||||
crate::RawIngestRouteId::StandardBlockDirect,
|
||||
network,
|
||||
&[(capabilities.has_standard_block, crate::RawIngestRouteUnavailableReason::MissingWsBlockCapability)],
|
||||
),
|
||||
];
|
||||
if capabilities.helius_source_declared {
|
||||
routes.push(route_with_requirements(
|
||||
crate::RawIngestRouteId::HeliusTransactionHydrated,
|
||||
network,
|
||||
&[
|
||||
(capabilities.has_helius_transaction, helius_reason),
|
||||
(capabilities.has_get_transaction, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction),
|
||||
],
|
||||
));
|
||||
}
|
||||
routes.push(route_with_requirements(
|
||||
crate::RawIngestRouteId::HttpBlockPolling,
|
||||
network,
|
||||
&[(capabilities.has_http_block_scan, crate::RawIngestRouteUnavailableReason::MissingHttpBlockScan)],
|
||||
));
|
||||
return routes;
|
||||
}
|
||||
|
||||
fn route_with_requirements(
|
||||
route_id: crate::RawIngestRouteId,
|
||||
network: &str,
|
||||
requirements: &[(bool, crate::RawIngestRouteUnavailableReason)],
|
||||
) -> crate::RawIngestRouteDto {
|
||||
for (available, reason) in requirements.iter().copied() {
|
||||
if !available {
|
||||
return crate::RawIngestRouteDto::unavailable(route_id, std::option::Option::Some(network), reason);
|
||||
}
|
||||
}
|
||||
return crate::RawIngestRouteDto::configured(route_id, network);
|
||||
}
|
||||
|
||||
fn unavailable_profile(
|
||||
profile_id: &str,
|
||||
is_default: bool,
|
||||
network: std::option::Option<&str>,
|
||||
reason: crate::RawIngestRouteUnavailableReason,
|
||||
) -> crate::RawIngestProfileInventoryDto {
|
||||
let routes = crate::RawIngestRouteId::all()
|
||||
.into_iter()
|
||||
.map(|route_id| return crate::RawIngestRouteDto::unavailable(route_id, network, reason))
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
return crate::RawIngestProfileInventoryDto { is_default, network: network.map(str::to_owned), profile_id: profile_id.to_owned(), routes };
|
||||
}
|
||||
|
||||
fn resolution_error_reason(error: &ksp_core_lib::Error) -> crate::RawIngestRouteUnavailableReason {
|
||||
if error.code() == ksp_config_lib::ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING {
|
||||
return crate::RawIngestRouteUnavailableReason::MissingRequiredSecret;
|
||||
}
|
||||
return crate::RawIngestRouteUnavailableReason::ProfileUnresolved;
|
||||
}
|
||||
|
||||
fn transport_matches_network(transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) -> bool {
|
||||
let networks = enabled_transport_networks(transport);
|
||||
return networks.len() == 1 && networks.contains(network);
|
||||
}
|
||||
|
||||
fn enabled_transport_networks(transport: &ksp_config_lib::ResolvedTransportConfig) -> std::collections::BTreeSet<String> {
|
||||
let mut networks = std::collections::BTreeSet::<String>::new();
|
||||
for endpoint in transport.http_settings().endpoints() {
|
||||
if endpoint.enabled() {
|
||||
networks.insert(endpoint.cluster().as_str().to_owned());
|
||||
}
|
||||
}
|
||||
if let std::option::Option::Some(settings) = transport.ws_settings() {
|
||||
for endpoint in settings.endpoints() {
|
||||
if endpoint.enabled() {
|
||||
networks.insert(endpoint.cluster().as_str().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let std::option::Option::Some(settings) = transport.grpc_settings() {
|
||||
for endpoint in settings.endpoints() {
|
||||
if endpoint.enabled() {
|
||||
networks.insert(endpoint.cluster().as_str().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
return networks;
|
||||
}
|
||||
|
||||
fn has_ws_capability(
|
||||
transport: &ksp_config_lib::ResolvedTransportConfig,
|
||||
network: &str,
|
||||
protocol: ksp_onchain_transport_lib::WsProtocolKind,
|
||||
capability: ksp_onchain_transport_lib::WsSubscriptionKind,
|
||||
) -> bool {
|
||||
let settings = match transport.ws_settings() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return false,
|
||||
};
|
||||
for endpoint in settings.endpoints() {
|
||||
if endpoint.enabled() && endpoint.cluster().as_str() == network && endpoint.protocol() == protocol && endpoint.supports_subscription(capability) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn has_yellowstone_grpc(transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) -> bool {
|
||||
let settings = match transport.grpc_settings() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return false,
|
||||
};
|
||||
return settings.endpoints().iter().any(|endpoint| return endpoint.enabled() && endpoint.cluster().as_str() == network);
|
||||
}
|
||||
|
||||
fn http_role_supports_methods(settings: &ksp_onchain_transport_lib::HttpTransportSettings, methods: &[&'static str]) -> bool {
|
||||
let mut request_kinds = std::vec::Vec::<&'static str>::with_capacity(methods.len());
|
||||
for method_name in methods {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name);
|
||||
let descriptor = match descriptor {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return false,
|
||||
};
|
||||
request_kinds.push(descriptor.request_kind());
|
||||
}
|
||||
let mut role_names = std::collections::BTreeSet::<String>::new();
|
||||
for endpoint in settings.endpoints() {
|
||||
if !endpoint.enabled() {
|
||||
continue;
|
||||
}
|
||||
for role in endpoint.roles() {
|
||||
if role.enabled() {
|
||||
role_names.insert(role.role().as_str().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
for role_name in role_names {
|
||||
let mut complete = true;
|
||||
for request_kind in &request_kinds {
|
||||
if !http_role_supports_request_kind(settings, role_name.as_str(), request_kind) {
|
||||
complete = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if complete {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn http_role_supports_request_kind(settings: &ksp_onchain_transport_lib::HttpTransportSettings, role_name: &str, request_kind: &str) -> bool {
|
||||
for endpoint in settings.endpoints() {
|
||||
if !endpoint.enabled() {
|
||||
continue;
|
||||
}
|
||||
for role in endpoint.roles() {
|
||||
if !role.enabled() || role.role().as_str() != role_name {
|
||||
continue;
|
||||
}
|
||||
for capability in role.request_kinds() {
|
||||
if capability.is_wildcard() || capability.as_str() == request_kind {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn inventory_invalid(message: &'static str) -> ksp_core_lib::Result<CompositeProfileCatalog> {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_INVENTORY_INVALID, message));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/route_inventory.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,352 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_monitoring.rs
|
||||
// version: 1
|
||||
|
||||
//! Frontend-safe latest-value monitoring projection for Raw Transaction Ingest Desk route Workers.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Stable Tauri event carrying one complete latest-value route monitoring projection.
|
||||
pub(crate) const RAW_INGEST_ROUTE_STATUS_EVENT_NAME: &str = "ksp-raw-ingest-route-status";
|
||||
|
||||
/// Frontend-safe bounded projection of one run-local continuity gap.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/route_monitoring/RawIngestRouteGapDto.ts")]
|
||||
pub(crate) struct RawIngestRouteGapDto {
|
||||
/// Run-local gap identifier encoded as decimal text to avoid JavaScript integer truncation.
|
||||
pub(crate) gap_id: String,
|
||||
/// Inclusive first slot encoded as decimal text.
|
||||
pub(crate) start_slot: String,
|
||||
/// Inclusive last slot encoded as decimal text.
|
||||
pub(crate) end_slot: String,
|
||||
/// Stable source-neutral gap lifecycle code.
|
||||
pub(crate) state: String,
|
||||
/// Stable source-neutral reason code.
|
||||
pub(crate) reason: String,
|
||||
/// Stable source-neutral latest repair mechanism code, when any.
|
||||
pub(crate) last_method: std::option::Option<String>,
|
||||
}
|
||||
|
||||
/// Frontend-safe latest-value monitoring projection of one independent route Worker.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/route_monitoring/RawIngestRouteMonitoringDto.ts")]
|
||||
pub(crate) struct RawIngestRouteMonitoringDto {
|
||||
/// Confirmed/finalized commitment bound to this Worker run.
|
||||
pub(crate) commitment: crate::RawIngestCommitment,
|
||||
/// Inventory generation revalidated before this Worker started.
|
||||
pub(crate) inventory_generation: u32,
|
||||
/// Logical network shared by Worker and Store.
|
||||
pub(crate) network: String,
|
||||
/// Safe logical network-profile identifier.
|
||||
pub(crate) profile_id: String,
|
||||
/// Stable logical route identifier.
|
||||
pub(crate) route_id: crate::RawIngestRouteId,
|
||||
/// Monotone Worker snapshot sequence encoded as decimal text.
|
||||
pub(crate) sequence: String,
|
||||
/// Safe application-owned lifecycle projection.
|
||||
pub(crate) state: crate::RawIngestRouteState,
|
||||
/// Stable generic Worker health code.
|
||||
pub(crate) health: String,
|
||||
/// Stable generic Worker activity code.
|
||||
pub(crate) activity: String,
|
||||
/// Whether this latest Worker snapshot is terminal.
|
||||
pub(crate) terminal: bool,
|
||||
/// Stable terminal fault domain when lifecycle is faulted.
|
||||
pub(crate) fault_domain: std::option::Option<String>,
|
||||
/// Stable terminal fault code when lifecycle is faulted.
|
||||
pub(crate) fault_code: std::option::Option<String>,
|
||||
/// Configured bounded admission queue capacity.
|
||||
pub(crate) admission_queue_capacity: u32,
|
||||
/// Latest admission queue depth.
|
||||
pub(crate) admission_queue_depth: u32,
|
||||
/// Configured Store persistence concurrency.
|
||||
pub(crate) persistence_concurrency: u32,
|
||||
/// Latest number of in-flight Store persistence operations.
|
||||
pub(crate) in_flight_persistence: u32,
|
||||
/// Total admitted ingress entries encoded as decimal text.
|
||||
pub(crate) admitted_total: String,
|
||||
/// Total canonicalized ingress entries encoded as decimal text.
|
||||
pub(crate) canonicalized_total: String,
|
||||
/// Total successful Store persistence outcomes encoded as decimal text.
|
||||
pub(crate) persisted_total: String,
|
||||
/// Total newly inserted canonical RAW entities encoded as decimal text.
|
||||
pub(crate) entity_inserted_total: String,
|
||||
/// Total canonical RAW entities already present encoded as decimal text.
|
||||
pub(crate) entity_already_present_total: String,
|
||||
/// Total purge tombstones respected by persistence encoded as decimal text.
|
||||
pub(crate) entity_skipped_purged_total: String,
|
||||
/// Total newly inserted acquisition observations encoded as decimal text.
|
||||
pub(crate) observation_inserted_total: String,
|
||||
/// Total acquisition observations already present encoded as decimal text.
|
||||
pub(crate) observation_already_present_total: String,
|
||||
/// Total durable content conflicts encoded as decimal text.
|
||||
pub(crate) content_conflict_total: String,
|
||||
/// Total non-conflict Store failures encoded as decimal text.
|
||||
pub(crate) store_failure_total: String,
|
||||
/// Total source-task failures encoded as decimal text.
|
||||
pub(crate) source_failure_total: String,
|
||||
/// Total bounded-admission backpressure waits encoded as decimal text.
|
||||
pub(crate) backpressure_wait_total: String,
|
||||
/// Latest number of source signals awaiting hydration/admission processing.
|
||||
pub(crate) hydration_pending: u32,
|
||||
/// Run-local processing frontier encoded as decimal text when established.
|
||||
pub(crate) processing_frontier_slot: std::option::Option<String>,
|
||||
/// Oldest slot still owning pending source work encoded as decimal text when present.
|
||||
pub(crate) oldest_pending_slot: std::option::Option<String>,
|
||||
/// Whether continuity health policy has emitted a run-local projection.
|
||||
pub(crate) continuity_policy_observed: bool,
|
||||
/// Latest run-local continuity frontier encoded as decimal text when established.
|
||||
pub(crate) continuity_frontier_slot: std::option::Option<String>,
|
||||
/// Whether one or more continuity gaps are currently open.
|
||||
pub(crate) continuity_has_open_gaps: bool,
|
||||
/// Whether all failed-source continuity obligations have been reconciled.
|
||||
pub(crate) failed_source_losses_reconciled: bool,
|
||||
/// Whether current coverage proves the target can continue into future slots.
|
||||
pub(crate) future_target_coverage: bool,
|
||||
/// Stable source-neutral latest productive-source lifecycle code.
|
||||
pub(crate) source_state: std::option::Option<String>,
|
||||
/// Configured logical live-source count.
|
||||
pub(crate) source_total: u32,
|
||||
/// Latest Active source count.
|
||||
pub(crate) source_active: u32,
|
||||
/// Latest Reconnecting source count.
|
||||
pub(crate) source_reconnecting: u32,
|
||||
/// Latest Failed source count.
|
||||
pub(crate) source_failed: u32,
|
||||
/// Successful source reconnect count encoded as decimal text.
|
||||
pub(crate) source_reconnect_total: String,
|
||||
/// Replay-bearing reconnect attempt count encoded as decimal text.
|
||||
pub(crate) source_replay_attempt_total: String,
|
||||
/// Proven replay-retention continuity gap count encoded as decimal text.
|
||||
pub(crate) source_continuity_gap_total: String,
|
||||
/// Number of currently open continuity gaps.
|
||||
pub(crate) open_gap_count: u32,
|
||||
/// Number of continuity gaps currently under repair.
|
||||
pub(crate) repairing_gap_count: u32,
|
||||
/// Cumulative repaired-gap count encoded as decimal text.
|
||||
pub(crate) repaired_gap_total: String,
|
||||
/// Cumulative unresolved-gap count encoded as decimal text.
|
||||
pub(crate) unresolved_gap_total: String,
|
||||
/// Cumulative replay repair count encoded as decimal text.
|
||||
pub(crate) replay_repair_total: String,
|
||||
/// Cumulative redundant-coverage repair count encoded as decimal text.
|
||||
pub(crate) redundant_coverage_repair_total: String,
|
||||
/// Cumulative bounded HTTP scan repair count encoded as decimal text.
|
||||
pub(crate) http_scan_repair_total: String,
|
||||
/// Cumulative direct block-fetch repair count encoded as decimal text.
|
||||
pub(crate) repair_block_fetch_total: String,
|
||||
/// Cumulative transaction-hydration repair count encoded as decimal text.
|
||||
pub(crate) repair_transaction_hydration_total: String,
|
||||
/// Oldest currently open gap start slot encoded as decimal text when present.
|
||||
pub(crate) oldest_open_gap_start_slot: std::option::Option<String>,
|
||||
/// Bounded source-neutral details for current and recent continuity gaps.
|
||||
pub(crate) gaps: std::vec::Vec<crate::RawIngestRouteGapDto>,
|
||||
}
|
||||
|
||||
/// Projects one complete concrete Worker snapshot to the safe route monitoring contract.
|
||||
pub(crate) fn project_route_monitoring(
|
||||
runtime: &crate::RawIngestRouteRuntimeDto,
|
||||
snapshot: &ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSnapshot,
|
||||
) -> ksp_core_lib::Result<crate::RawIngestRouteMonitoringDto> {
|
||||
let worker = snapshot.worker_snapshot();
|
||||
let fault = worker.state().fault_code();
|
||||
let admission_queue_capacity = usize_to_u32(snapshot.admission_queue_capacity(), "admission_queue_capacity");
|
||||
let admission_queue_capacity = match admission_queue_capacity {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let admission_queue_depth = usize_to_u32(snapshot.admission_queue_depth(), "admission_queue_depth");
|
||||
let admission_queue_depth = match admission_queue_depth {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let persistence_concurrency = usize_to_u32(snapshot.persistence_concurrency(), "persistence_concurrency");
|
||||
let persistence_concurrency = match persistence_concurrency {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let in_flight_persistence = usize_to_u32(snapshot.in_flight_persistence(), "in_flight_persistence");
|
||||
let in_flight_persistence = match in_flight_persistence {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let hydration_pending = usize_to_u32(snapshot.hydration_pending(), "hydration_pending");
|
||||
let hydration_pending = match hydration_pending {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let source_total = usize_to_u32(snapshot.source_total(), "source_total");
|
||||
let source_total = match source_total {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let source_active = usize_to_u32(snapshot.source_active(), "source_active");
|
||||
let source_active = match source_active {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let source_reconnecting = usize_to_u32(snapshot.source_reconnecting(), "source_reconnecting");
|
||||
let source_reconnecting = match source_reconnecting {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let source_failed = usize_to_u32(snapshot.source_failed(), "source_failed");
|
||||
let source_failed = match source_failed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let open_gap_count = usize_to_u32(snapshot.open_gap_count(), "open_gap_count");
|
||||
let open_gap_count = match open_gap_count {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let repairing_gap_count = usize_to_u32(snapshot.repairing_gap_count(), "repairing_gap_count");
|
||||
let repairing_gap_count = match repairing_gap_count {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let gaps = snapshot.gaps().iter().map(project_gap).collect::<std::vec::Vec<_>>();
|
||||
return std::result::Result::Ok(crate::RawIngestRouteMonitoringDto {
|
||||
commitment: runtime.commitment,
|
||||
inventory_generation: runtime.inventory_generation,
|
||||
network: runtime.network.clone(),
|
||||
profile_id: runtime.profile_id.clone(),
|
||||
route_id: runtime.route_id,
|
||||
sequence: worker.sequence().value().to_string(),
|
||||
state: project_worker_state(worker.state()),
|
||||
health: worker.health().code().to_owned(),
|
||||
activity: worker.activity().code().to_owned(),
|
||||
terminal: worker.state().is_terminal(),
|
||||
fault_domain: fault.map(|code| return code.domain().to_owned()),
|
||||
fault_code: fault.map(|code| return code.code().to_owned()),
|
||||
admission_queue_capacity,
|
||||
admission_queue_depth,
|
||||
persistence_concurrency,
|
||||
in_flight_persistence,
|
||||
admitted_total: snapshot.admitted_total().to_string(),
|
||||
canonicalized_total: snapshot.canonicalized_total().to_string(),
|
||||
persisted_total: snapshot.persisted_total().to_string(),
|
||||
entity_inserted_total: snapshot.entity_inserted_total().to_string(),
|
||||
entity_already_present_total: snapshot.entity_already_present_total().to_string(),
|
||||
entity_skipped_purged_total: snapshot.entity_skipped_purged_total().to_string(),
|
||||
observation_inserted_total: snapshot.observation_inserted_total().to_string(),
|
||||
observation_already_present_total: snapshot.observation_already_present_total().to_string(),
|
||||
content_conflict_total: snapshot.content_conflict_total().to_string(),
|
||||
store_failure_total: snapshot.store_failure_total().to_string(),
|
||||
source_failure_total: snapshot.source_failure_total().to_string(),
|
||||
backpressure_wait_total: snapshot.backpressure_wait_total().to_string(),
|
||||
hydration_pending,
|
||||
processing_frontier_slot: slot_text(snapshot.processing_frontier_slot()),
|
||||
oldest_pending_slot: slot_text(snapshot.oldest_pending_slot()),
|
||||
continuity_policy_observed: snapshot.continuity_policy_observed(),
|
||||
continuity_frontier_slot: slot_text(snapshot.continuity_frontier_slot()),
|
||||
continuity_has_open_gaps: snapshot.continuity_has_open_gaps(),
|
||||
failed_source_losses_reconciled: snapshot.failed_source_losses_reconciled(),
|
||||
future_target_coverage: snapshot.future_target_coverage(),
|
||||
source_state: snapshot.source_state().map(|state| return source_state_code(state).to_owned()),
|
||||
source_total,
|
||||
source_active,
|
||||
source_reconnecting,
|
||||
source_failed,
|
||||
source_reconnect_total: snapshot.source_reconnect_total().to_string(),
|
||||
source_replay_attempt_total: snapshot.source_replay_attempt_total().to_string(),
|
||||
source_continuity_gap_total: snapshot.source_continuity_gap_total().to_string(),
|
||||
open_gap_count,
|
||||
repairing_gap_count,
|
||||
repaired_gap_total: snapshot.repaired_gap_total().to_string(),
|
||||
unresolved_gap_total: snapshot.unresolved_gap_total().to_string(),
|
||||
replay_repair_total: snapshot.replay_repair_total().to_string(),
|
||||
redundant_coverage_repair_total: snapshot.redundant_coverage_repair_total().to_string(),
|
||||
http_scan_repair_total: snapshot.http_scan_repair_total().to_string(),
|
||||
repair_block_fetch_total: snapshot.repair_block_fetch_total().to_string(),
|
||||
repair_transaction_hydration_total: snapshot.repair_transaction_hydration_total().to_string(),
|
||||
oldest_open_gap_start_slot: slot_text(snapshot.oldest_open_gap_start_slot()),
|
||||
gaps,
|
||||
});
|
||||
}
|
||||
|
||||
fn project_gap(gap: &ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapSnapshot) -> crate::RawIngestRouteGapDto {
|
||||
return crate::RawIngestRouteGapDto {
|
||||
gap_id: gap.gap_id().value().to_string(),
|
||||
start_slot: gap.start_slot().to_string(),
|
||||
end_slot: gap.end_slot().to_string(),
|
||||
state: gap_state_code(gap.state()).to_owned(),
|
||||
reason: gap_reason_code(gap.reason()).to_owned(),
|
||||
last_method: gap.last_method().map(|method| return repair_method_code(method).to_owned()),
|
||||
};
|
||||
}
|
||||
|
||||
fn project_worker_state(state: ksp_worker_api::WorkerState) -> crate::RawIngestRouteState {
|
||||
return match state {
|
||||
ksp_worker_api::WorkerState::Created | ksp_worker_api::WorkerState::Starting => crate::RawIngestRouteState::Starting,
|
||||
ksp_worker_api::WorkerState::Running => crate::RawIngestRouteState::Running,
|
||||
ksp_worker_api::WorkerState::Stopping => crate::RawIngestRouteState::Stopping,
|
||||
ksp_worker_api::WorkerState::Stopped => crate::RawIngestRouteState::Stopped,
|
||||
ksp_worker_api::WorkerState::Faulted(_) => crate::RawIngestRouteState::Faulted,
|
||||
_ => crate::RawIngestRouteState::Faulted,
|
||||
};
|
||||
}
|
||||
|
||||
fn source_state_code(state: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState) -> &'static str {
|
||||
return match state {
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Active => "active",
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Reconnecting => "reconnecting",
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Closing => "closing",
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Closed => "closed",
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Failed => "failed",
|
||||
};
|
||||
}
|
||||
|
||||
fn gap_state_code(state: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState) -> &'static str {
|
||||
return match state {
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState::Pending => "pending",
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState::Repairing => "repairing",
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState::Repaired => "repaired",
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState::Unresolved => "unresolved",
|
||||
};
|
||||
}
|
||||
|
||||
fn gap_reason_code(reason: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason) -> &'static str {
|
||||
return match reason {
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::HttpProducedBlockUnavailable => "http_produced_block_unavailable",
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::KnownReferenceMissing => "known_reference_missing",
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::SourceFailure => "source_failure",
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::TransportOverflow => "transport_overflow",
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::WebSocketReconnect => "websocket_reconnect",
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::YellowstoneRetention => "yellowstone_retention",
|
||||
};
|
||||
}
|
||||
|
||||
fn repair_method_code(method: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod) -> &'static str {
|
||||
return match method {
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::Replay => "replay",
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::RedundantCoverage => "redundant_coverage",
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::HttpScan => "http_scan",
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::BlockFetch => "block_fetch",
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::TransactionHydration => "transaction_hydration",
|
||||
};
|
||||
}
|
||||
|
||||
fn slot_text(value: std::option::Option<u64>) -> std::option::Option<String> {
|
||||
return value.map(|slot| return slot.to_string());
|
||||
}
|
||||
|
||||
fn usize_to_u32(value: usize, field: &'static str) -> ksp_core_lib::Result<u32> {
|
||||
let converted = u32::try_from(value);
|
||||
return match converted {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_APP_STATE_INVALID,
|
||||
"Raw Transaction Ingest Desk cannot project a bounded Worker counter to the frontend",
|
||||
)
|
||||
.with_context("field", field)
|
||||
.with_source(error),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/route_monitoring.rs"]
|
||||
mod tests;
|
||||
910
crates/ksp-app-raw-transaction-ingest-desk/src/route_runtime.rs
Normal file
910
crates/ksp-app-raw-transaction-ingest-desk/src/route_runtime.rs
Normal file
@@ -0,0 +1,910 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_runtime.rs
|
||||
// version: 8
|
||||
|
||||
//! Multi-route Store and independent Worker lifecycle owned by Raw Transaction Ingest Desk.
|
||||
|
||||
const ROUTE_SHUTDOWN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
|
||||
const ROUTE_STOP_CLEANUP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
|
||||
const STORE_RECLAIM_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(5);
|
||||
const STORE_RECLAIM_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(1);
|
||||
|
||||
struct RouteRuntimeInner {
|
||||
last_monitoring: std::vec::Vec<crate::RawIngestRouteMonitoringDto>,
|
||||
last_terminals: std::vec::Vec<crate::RawIngestRouteRuntimeDto>,
|
||||
network: std::option::Option<String>,
|
||||
next_sequence: u64,
|
||||
routes: std::vec::Vec<RouteRuntimeSlot>,
|
||||
shutting_down: bool,
|
||||
store: std::option::Option<std::sync::Arc<ksp_store_lib::Store>>,
|
||||
store_closing_token: std::option::Option<u64>,
|
||||
store_opening_token: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
/// Shared multi-route runtime state. Each route owns an independent Worker while same-network routes share one Store.
|
||||
pub(crate) struct RouteRuntimeState {
|
||||
inner: std::sync::Mutex<RouteRuntimeInner>,
|
||||
}
|
||||
|
||||
impl crate::RouteRuntimeState {
|
||||
/// Creates an idle multi-route runtime state without Store or Worker ownership.
|
||||
#[must_use]
|
||||
pub(crate) fn new() -> Self {
|
||||
return Self {
|
||||
inner: std::sync::Mutex::new(RouteRuntimeInner {
|
||||
last_monitoring: std::vec::Vec::new(),
|
||||
last_terminals: std::vec::Vec::new(),
|
||||
network: std::option::Option::None,
|
||||
next_sequence: 0,
|
||||
routes: std::vec::Vec::new(),
|
||||
shutting_down: false,
|
||||
store: std::option::Option::None,
|
||||
store_closing_token: std::option::Option::None,
|
||||
store_opening_token: std::option::Option::None,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/// Rejects new route Start work after application shutdown has closed admission.
|
||||
pub(crate) fn ensure_start_admission_open(&self) -> ksp_core_lib::Result<()> {
|
||||
let inner = self.inner.lock();
|
||||
let inner = match inner {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
|
||||
};
|
||||
if inner.shutting_down {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN,
|
||||
"Raw Transaction Ingest Desk cannot start a route while application shutdown is in progress",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn reserve(&self, prepared: &crate::PreparedRouteStart) -> ksp_core_lib::Result<RouteRuntimeReservation> {
|
||||
let inner = self.inner.lock();
|
||||
let mut inner = match inner {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
|
||||
};
|
||||
if inner.shutting_down {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN,
|
||||
"Raw Transaction Ingest Desk cannot start a route while application shutdown is in progress",
|
||||
));
|
||||
}
|
||||
if inner.store_closing_token.is_some() {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_RUNTIME_ACTIVE,
|
||||
"Raw Transaction Ingest Desk Store is still closing after the previous route set",
|
||||
));
|
||||
}
|
||||
if inner.routes.iter().any(|slot| return slot.matches_identity(prepared.profile_id.as_str(), prepared.route_id)) {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_RUNTIME_ACTIVE,
|
||||
"Raw Transaction Ingest Desk already owns one Worker for the selected logical route",
|
||||
));
|
||||
}
|
||||
if let std::option::Option::Some(network) = inner.network.as_deref()
|
||||
&& network != prepared.network.as_str()
|
||||
{
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_RUNTIME_NETWORK_MISMATCH,
|
||||
"Raw Transaction Ingest Desk cannot share one Store across different logical networks",
|
||||
));
|
||||
}
|
||||
let needs_store_open = inner.store.is_none();
|
||||
if needs_store_open && inner.store_opening_token.is_some() {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_RUNTIME_ACTIVE,
|
||||
"Raw Transaction Ingest Desk same-network Store is still opening",
|
||||
));
|
||||
}
|
||||
let sequence = match inner.next_sequence.checked_add(1) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_RUNTIME_START_FAILED,
|
||||
"Raw Transaction Ingest Desk Worker sequence is exhausted",
|
||||
));
|
||||
},
|
||||
};
|
||||
let worker_id = ksp_worker_api::WorkerId::new(format!("raw-ingest-desk-{sequence}"));
|
||||
let worker_id = match worker_id {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_RUNTIME_START_FAILED, "Cannot create Raw Transaction Ingest Desk Worker identity")
|
||||
.with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
let identity = RouteRuntimeIdentity {
|
||||
commitment: prepared.commitment,
|
||||
inventory_generation: prepared.inventory_generation,
|
||||
network: prepared.network.clone(),
|
||||
profile_id: prepared.profile_id.clone(),
|
||||
route_id: prepared.route_id,
|
||||
};
|
||||
if inner.network.is_none() {
|
||||
inner.last_monitoring.clear();
|
||||
inner.last_terminals.clear();
|
||||
inner.network = std::option::Option::Some(prepared.network.clone());
|
||||
}
|
||||
inner.last_monitoring.retain(|status| return !identity.matches_monitoring(status));
|
||||
inner.last_terminals.retain(|terminal| return !identity.matches_dto(terminal));
|
||||
inner.next_sequence = sequence;
|
||||
if needs_store_open {
|
||||
inner.store_opening_token = std::option::Option::Some(sequence);
|
||||
}
|
||||
inner.routes.push(RouteRuntimeSlot::Starting { identity: identity.clone(), stop_requested: false, token: sequence });
|
||||
return std::result::Result::Ok(RouteRuntimeReservation { identity, needs_store_open, token: sequence, worker_id });
|
||||
}
|
||||
|
||||
fn install_store(&self, token: u64, store: std::sync::Arc<ksp_store_lib::Store>) -> ksp_core_lib::Result<()> {
|
||||
let inner = self.inner.lock();
|
||||
let mut inner = match inner {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
|
||||
};
|
||||
if inner.store_opening_token != std::option::Option::Some(token) || inner.store.is_some() {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_RUNTIME_STATE_INVALID,
|
||||
"Raw Transaction Ingest Desk shared Store reservation changed before installation",
|
||||
));
|
||||
}
|
||||
inner.store = std::option::Option::Some(store);
|
||||
inner.store_opening_token = std::option::Option::None;
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn shared_store(&self, expected_network: &str) -> ksp_core_lib::Result<std::sync::Arc<ksp_store_lib::Store>> {
|
||||
let inner = self.inner.lock();
|
||||
let inner = match inner {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
|
||||
};
|
||||
if inner.network.as_deref() != std::option::Option::Some(expected_network) {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_RUNTIME_NETWORK_MISMATCH,
|
||||
"Raw Transaction Ingest Desk shared Store network no longer matches the selected route",
|
||||
));
|
||||
}
|
||||
return match &inner.store {
|
||||
std::option::Option::Some(store) => std::result::Result::Ok(std::sync::Arc::clone(store)),
|
||||
std::option::Option::None => std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_RUNTIME_STATE_INVALID,
|
||||
"Raw Transaction Ingest Desk shared Store is not installed",
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
fn activate(
|
||||
&self,
|
||||
reservation: &RouteRuntimeReservation,
|
||||
handle: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHandle,
|
||||
) -> ksp_core_lib::Result<crate::RawIngestRouteRuntimeDto> {
|
||||
let inner = self.inner.lock();
|
||||
let mut inner = match inner {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
|
||||
};
|
||||
let position = inner.routes.iter().position(|slot| return slot.starting_token() == std::option::Option::Some(reservation.token));
|
||||
let position = match position {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_RUNTIME_STATE_INVALID,
|
||||
"Raw Transaction Ingest Desk route reservation changed before Worker activation",
|
||||
));
|
||||
},
|
||||
};
|
||||
let stop_requested = match &inner.routes[position] {
|
||||
RouteRuntimeSlot::Starting { stop_requested, .. } => *stop_requested,
|
||||
RouteRuntimeSlot::Active { .. } => false,
|
||||
};
|
||||
let should_stop = stop_requested || inner.shutting_down;
|
||||
if should_stop {
|
||||
let _accepted = handle.request_stop();
|
||||
}
|
||||
let state = if should_stop {
|
||||
crate::RawIngestRouteState::Stopping
|
||||
} else {
|
||||
project_worker_state(handle.snapshot_source().current().worker_snapshot().state())
|
||||
};
|
||||
inner.routes[position] = RouteRuntimeSlot::Active { handle, identity: reservation.identity.clone(), token: reservation.token };
|
||||
return std::result::Result::Ok(reservation.identity.dto(state));
|
||||
}
|
||||
|
||||
fn rollback(&self, token: u64) -> ksp_core_lib::Result<std::option::Option<std::sync::Arc<ksp_store_lib::Store>>> {
|
||||
let inner = self.inner.lock();
|
||||
let mut inner = match inner {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
|
||||
};
|
||||
let cancelled_terminal = inner.routes.iter().find(|slot| return slot.token() == token).and_then(RouteRuntimeSlot::cancelled_start_terminal);
|
||||
inner.routes.retain(|slot| return slot.token() != token);
|
||||
if let std::option::Option::Some(terminal) = cancelled_terminal {
|
||||
inner.last_terminals.retain(|previous| return previous.profile_id != terminal.profile_id || previous.route_id != terminal.route_id);
|
||||
inner.last_terminals.push(terminal);
|
||||
}
|
||||
if inner.store_opening_token == std::option::Option::Some(token) {
|
||||
inner.store_opening_token = std::option::Option::None;
|
||||
}
|
||||
if inner.routes.is_empty() {
|
||||
let store = inner.store.take();
|
||||
if store.is_some() {
|
||||
inner.store_closing_token = std::option::Option::Some(token);
|
||||
} else {
|
||||
inner.network = std::option::Option::None;
|
||||
}
|
||||
return std::result::Result::Ok(store);
|
||||
}
|
||||
return std::result::Result::Ok(std::option::Option::None);
|
||||
}
|
||||
|
||||
fn finish(
|
||||
&self,
|
||||
token: u64,
|
||||
terminal: crate::RawIngestRouteRuntimeDto,
|
||||
monitoring: std::option::Option<crate::RawIngestRouteMonitoringDto>,
|
||||
) -> ksp_core_lib::Result<std::option::Option<std::sync::Arc<ksp_store_lib::Store>>> {
|
||||
let inner = self.inner.lock();
|
||||
let mut inner = match inner {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
|
||||
};
|
||||
let before = inner.routes.len();
|
||||
inner.routes.retain(|slot| return slot.token() != token);
|
||||
if inner.routes.len() == before {
|
||||
return std::result::Result::Ok(std::option::Option::None);
|
||||
}
|
||||
inner.last_terminals.retain(|previous| return previous.profile_id != terminal.profile_id || previous.route_id != terminal.route_id);
|
||||
inner.last_terminals.push(terminal);
|
||||
if let std::option::Option::Some(status) = monitoring {
|
||||
inner.last_monitoring.retain(|previous| return previous.profile_id != status.profile_id || previous.route_id != status.route_id);
|
||||
inner.last_monitoring.push(status);
|
||||
}
|
||||
if inner.routes.is_empty() {
|
||||
let store = inner.store.take();
|
||||
if store.is_some() {
|
||||
inner.store_closing_token = std::option::Option::Some(token);
|
||||
} else {
|
||||
inner.network = std::option::Option::None;
|
||||
}
|
||||
return std::result::Result::Ok(store);
|
||||
}
|
||||
return std::result::Result::Ok(std::option::Option::None);
|
||||
}
|
||||
|
||||
fn complete_store_close(&self, token: u64) {
|
||||
let inner = self.inner.lock();
|
||||
let mut inner = match inner {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
if inner.store_closing_token == std::option::Option::Some(token) {
|
||||
inner.store_closing_token = std::option::Option::None;
|
||||
if inner.routes.is_empty() && inner.store.is_none() && inner.store_opening_token.is_none() {
|
||||
inner.network = std::option::Option::None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns complete latest-value monitoring projections for active and recent terminal routes in the current runtime session.
|
||||
pub(crate) fn monitoring_statuses(&self) -> ksp_core_lib::Result<std::vec::Vec<crate::RawIngestRouteMonitoringDto>> {
|
||||
let inner = self.inner.lock();
|
||||
let inner = match inner {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
|
||||
};
|
||||
let mut values = inner.last_monitoring.clone();
|
||||
for slot in &inner.routes {
|
||||
if let RouteRuntimeSlot::Active { handle, identity, .. } = slot {
|
||||
let snapshot = handle.snapshot_source().current();
|
||||
let runtime = identity.dto(project_worker_state(snapshot.worker_snapshot().state()));
|
||||
let projected = crate::project_route_monitoring(&runtime, &snapshot);
|
||||
let projected = match projected {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
values.retain(|previous| return previous.profile_id != projected.profile_id || previous.route_id != projected.route_id);
|
||||
values.push(projected);
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(values);
|
||||
}
|
||||
|
||||
/// Closes route Start admission exactly once and requests cooperative Stop for all active or still-starting routes.
|
||||
pub(crate) fn begin_shutdown(&self) -> ksp_core_lib::Result<bool> {
|
||||
let inner = self.inner.lock();
|
||||
let mut inner = match inner {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
|
||||
};
|
||||
if inner.shutting_down {
|
||||
return std::result::Result::Ok(false);
|
||||
}
|
||||
inner.shutting_down = true;
|
||||
for slot in &mut inner.routes {
|
||||
match slot {
|
||||
RouteRuntimeSlot::Starting { stop_requested, .. } => *stop_requested = true,
|
||||
RouteRuntimeSlot::Active { handle, .. } => {
|
||||
let _accepted = handle.request_stop();
|
||||
},
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(true);
|
||||
}
|
||||
|
||||
/// Waits until all owned route Workers and the shared Store have completed bounded application-shutdown cleanup.
|
||||
pub(crate) async fn shutdown_and_wait(&self) -> ksp_core_lib::Result<()> {
|
||||
let begin = self.begin_shutdown();
|
||||
if let std::result::Result::Err(error) = begin {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let started = std::time::Instant::now();
|
||||
loop {
|
||||
let complete = self.shutdown_complete();
|
||||
let complete = match complete {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if complete {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
if started.elapsed() >= ROUTE_SHUTDOWN_TIMEOUT {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_RUNTIME_SHUTDOWN_FAILED,
|
||||
"Raw Transaction Ingest Desk application shutdown did not release all route Workers and shared Store state before the bounded deadline",
|
||||
));
|
||||
}
|
||||
tokio::time::sleep(STORE_RECLAIM_POLL_INTERVAL).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn shutdown_complete(&self) -> ksp_core_lib::Result<bool> {
|
||||
let inner = self.inner.lock();
|
||||
let inner = match inner {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
|
||||
};
|
||||
return std::result::Result::Ok(
|
||||
inner.routes.is_empty() && inner.store.is_none() && inner.store_opening_token.is_none() && inner.store_closing_token.is_none(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Requests cooperative Stop for one exact logical route and waits for its Worker cleanup.
|
||||
pub(crate) async fn stop_and_wait(&self, request: &crate::RawIngestRouteStopRequestDto) -> ksp_core_lib::Result<crate::RawIngestRouteRuntimeDto> {
|
||||
let identity = request.validate_logical_identity();
|
||||
if let std::result::Result::Err(error) = identity {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let target = self.stop_target(request);
|
||||
let target = match target {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let (handle, identity, token) = match target {
|
||||
RouteStopTarget::Active { handle, identity, token } => (handle, identity, token),
|
||||
RouteStopTarget::Starting { identity, token } => return self.wait_route_cleanup(token, &identity).await,
|
||||
RouteStopTarget::Terminal(value) => return std::result::Result::Ok(value),
|
||||
RouteStopTarget::TerminalClosing => return self.wait_terminal_store_cleanup(request).await,
|
||||
};
|
||||
let _first_stop_request = handle.request_stop();
|
||||
if let std::result::Result::Err(error) = handle.wait_terminal().await {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_RUNTIME_STATE_INVALID, "Cannot observe terminal Raw Transaction Ingest Desk Worker state")
|
||||
.with_source(error),
|
||||
);
|
||||
}
|
||||
return self.wait_route_cleanup(token, &identity).await;
|
||||
}
|
||||
|
||||
fn stop_target(&self, request: &crate::RawIngestRouteStopRequestDto) -> ksp_core_lib::Result<RouteStopTarget> {
|
||||
let inner = self.inner.lock();
|
||||
let mut inner = match inner {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
|
||||
};
|
||||
let position = inner.routes.iter().position(|slot| return slot.matches_identity(request.profile_id.as_str(), request.route_id));
|
||||
if let std::option::Option::Some(position) = position {
|
||||
return match &mut inner.routes[position] {
|
||||
RouteRuntimeSlot::Active { handle, identity, token } => {
|
||||
std::result::Result::Ok(RouteStopTarget::Active { handle: handle.clone(), identity: identity.clone(), token: *token })
|
||||
},
|
||||
RouteRuntimeSlot::Starting { identity, stop_requested, token } => {
|
||||
*stop_requested = true;
|
||||
std::result::Result::Ok(RouteStopTarget::Starting { identity: identity.clone(), token: *token })
|
||||
},
|
||||
};
|
||||
}
|
||||
let terminal = inner.last_terminals.iter().find(|terminal| return terminal.profile_id == request.profile_id && terminal.route_id == request.route_id);
|
||||
return match terminal {
|
||||
std::option::Option::Some(value) if inner.store_closing_token.is_none() => std::result::Result::Ok(RouteStopTarget::Terminal(value.clone())),
|
||||
std::option::Option::Some(_) => std::result::Result::Ok(RouteStopTarget::TerminalClosing),
|
||||
std::option::Option::None => std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_RUNTIME_NOT_ACTIVE,
|
||||
"Raw Transaction Ingest Desk has no active Worker for the selected logical route",
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
async fn wait_route_cleanup(&self, token: u64, identity: &RouteRuntimeIdentity) -> ksp_core_lib::Result<crate::RawIngestRouteRuntimeDto> {
|
||||
let started = std::time::Instant::now();
|
||||
loop {
|
||||
let cleanup = self.route_cleanup_complete(token, identity);
|
||||
let cleanup = match cleanup {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if let std::option::Option::Some(value) = cleanup {
|
||||
return std::result::Result::Ok(value);
|
||||
}
|
||||
if started.elapsed() >= ROUTE_STOP_CLEANUP_TIMEOUT {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED,
|
||||
"Raw Transaction Ingest Desk terminal cleanup did not release the selected route",
|
||||
));
|
||||
}
|
||||
tokio::time::sleep(STORE_RECLAIM_POLL_INTERVAL).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_terminal_store_cleanup(&self, request: &crate::RawIngestRouteStopRequestDto) -> ksp_core_lib::Result<crate::RawIngestRouteRuntimeDto> {
|
||||
let started = std::time::Instant::now();
|
||||
loop {
|
||||
let terminal = self.late_terminal_after_store_cleanup(request);
|
||||
let terminal = match terminal {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if let std::option::Option::Some(value) = terminal {
|
||||
return std::result::Result::Ok(value);
|
||||
}
|
||||
if started.elapsed() >= ROUTE_STOP_CLEANUP_TIMEOUT {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED,
|
||||
"Raw Transaction Ingest Desk final shared Store cleanup did not complete for the selected terminal route",
|
||||
));
|
||||
}
|
||||
tokio::time::sleep(STORE_RECLAIM_POLL_INTERVAL).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn late_terminal_after_store_cleanup(
|
||||
&self,
|
||||
request: &crate::RawIngestRouteStopRequestDto,
|
||||
) -> ksp_core_lib::Result<std::option::Option<crate::RawIngestRouteRuntimeDto>> {
|
||||
let inner = self.inner.lock();
|
||||
let inner = match inner {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
|
||||
};
|
||||
let terminal = inner.last_terminals.iter().find(|terminal| return terminal.profile_id == request.profile_id && terminal.route_id == request.route_id);
|
||||
let terminal = match terminal {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_RUNTIME_NOT_ACTIVE,
|
||||
"Raw Transaction Ingest Desk terminal route is no longer retained",
|
||||
));
|
||||
},
|
||||
};
|
||||
if inner.store_closing_token.is_some() {
|
||||
return std::result::Result::Ok(std::option::Option::None);
|
||||
}
|
||||
return std::result::Result::Ok(std::option::Option::Some(terminal.clone()));
|
||||
}
|
||||
|
||||
fn route_cleanup_complete(
|
||||
&self,
|
||||
token: u64,
|
||||
identity: &RouteRuntimeIdentity,
|
||||
) -> ksp_core_lib::Result<std::option::Option<crate::RawIngestRouteRuntimeDto>> {
|
||||
let inner = self.inner.lock();
|
||||
let inner = match inner {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
|
||||
};
|
||||
if inner.routes.iter().any(|slot| return slot.token() == token) || inner.store_closing_token == std::option::Option::Some(token) {
|
||||
return std::result::Result::Ok(std::option::Option::None);
|
||||
}
|
||||
let terminal = inner.last_terminals.iter().find(|value| return identity.matches_dto(value));
|
||||
return std::result::Result::Ok(terminal.cloned());
|
||||
}
|
||||
}
|
||||
|
||||
/// Launch ownership returned only after shared Store readiness, Worker creation and route-handle installation have succeeded.
|
||||
pub(crate) struct RouteRuntimeLaunch {
|
||||
acknowledgement: crate::RawIngestRouteRuntimeDto,
|
||||
handle: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHandle,
|
||||
runtime_state: std::sync::Arc<crate::RouteRuntimeState>,
|
||||
token: u64,
|
||||
}
|
||||
|
||||
impl crate::RouteRuntimeLaunch {
|
||||
/// Returns the safe Start acknowledgement captured after the Worker handle was installed.
|
||||
#[must_use]
|
||||
pub(crate) fn acknowledgement(&self) -> crate::RawIngestRouteRuntimeDto {
|
||||
return self.acknowledgement.clone();
|
||||
}
|
||||
|
||||
/// Returns the complete safe logical route identity used to project Worker monitoring snapshots.
|
||||
#[must_use]
|
||||
pub(crate) fn monitoring_identity(&self) -> crate::RawIngestRouteRuntimeDto {
|
||||
return self.acknowledgement.clone();
|
||||
}
|
||||
|
||||
/// Returns an independent latest-value source for frontend-safe monitoring relay.
|
||||
#[must_use]
|
||||
pub(crate) fn monitoring_source(&self) -> ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSnapshotSource {
|
||||
return self.handle.snapshot_source();
|
||||
}
|
||||
|
||||
/// Waits for this Worker terminal state, removes only its route and closes Store only after the final same-network route terminates.
|
||||
pub(crate) async fn monitor(self) {
|
||||
let terminal = self.handle.wait_terminal().await;
|
||||
let snapshot = self.handle.snapshot_source().current();
|
||||
let worker = snapshot.worker_snapshot();
|
||||
let snapshot_fault = worker.state().fault_code();
|
||||
let snapshot_fault_domain = snapshot_fault.map_or("none", |code| return code.domain());
|
||||
let snapshot_fault_code = snapshot_fault.map_or("none", |code| return code.code());
|
||||
let terminal_state = match terminal {
|
||||
std::result::Result::Ok(state) => {
|
||||
ksp_logging_lib::info!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
|
||||
profile_id = self.acknowledgement.profile_id.as_str(),
|
||||
network = self.acknowledgement.network.as_str(),
|
||||
route_id = self.acknowledgement.route_id.as_str(),
|
||||
commitment = self.acknowledgement.commitment.as_str(),
|
||||
worker_state = state.code(),
|
||||
worker_health = worker.health().code(),
|
||||
worker_activity = worker.activity().code(),
|
||||
fault_domain = snapshot_fault_domain,
|
||||
fault_code = snapshot_fault_code,
|
||||
source_failure_total = snapshot.source_failure_total(),
|
||||
store_failure_total = snapshot.store_failure_total(),
|
||||
content_conflict_total = snapshot.content_conflict_total(),
|
||||
source_active = snapshot.source_active(),
|
||||
source_reconnecting = snapshot.source_reconnecting(),
|
||||
source_failed = snapshot.source_failed(),
|
||||
open_gap_count = snapshot.open_gap_count(),
|
||||
continuity_has_open_gaps = snapshot.continuity_has_open_gaps(),
|
||||
future_target_coverage = snapshot.future_target_coverage(),
|
||||
failed_source_losses_reconciled = snapshot.failed_source_losses_reconciled(),
|
||||
"Raw Transaction Ingest Desk route Worker reached terminal state"
|
||||
);
|
||||
project_worker_state(state)
|
||||
},
|
||||
std::result::Result::Err(error) => {
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
|
||||
profile_id = self.acknowledgement.profile_id.as_str(),
|
||||
network = self.acknowledgement.network.as_str(),
|
||||
route_id = self.acknowledgement.route_id.as_str(),
|
||||
commitment = self.acknowledgement.commitment.as_str(),
|
||||
error_domain = error.code().domain(),
|
||||
error_code = error.code().code(),
|
||||
worker_health = worker.health().code(),
|
||||
snapshot_fault_domain = snapshot_fault_domain,
|
||||
snapshot_fault_code = snapshot_fault_code,
|
||||
source_failure_total = snapshot.source_failure_total(),
|
||||
store_failure_total = snapshot.store_failure_total(),
|
||||
content_conflict_total = snapshot.content_conflict_total(),
|
||||
"Raw Transaction Ingest Desk terminal Worker wait failed"
|
||||
);
|
||||
crate::RawIngestRouteState::Faulted
|
||||
},
|
||||
};
|
||||
let mut terminal = self.acknowledgement;
|
||||
terminal.state = terminal_state;
|
||||
let monitoring = crate::project_route_monitoring(&terminal, &snapshot);
|
||||
let monitoring = match monitoring {
|
||||
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
||||
std::result::Result::Err(error) => {
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
|
||||
error_domain = error.code().domain(),
|
||||
error_code = error.code().code(),
|
||||
"Raw Transaction Ingest Desk could not retain terminal route monitoring projection"
|
||||
);
|
||||
std::option::Option::None
|
||||
},
|
||||
};
|
||||
let store = self.runtime_state.finish(self.token, terminal, monitoring);
|
||||
let store = match store {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
|
||||
error_domain = error.code().domain(),
|
||||
error_code = error.code().code(),
|
||||
"Raw Transaction Ingest Desk route cleanup state update failed"
|
||||
);
|
||||
return;
|
||||
},
|
||||
};
|
||||
if let std::option::Option::Some(store) = store {
|
||||
let close = close_store_arc(store).await;
|
||||
if let std::result::Result::Err(error) = close {
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
|
||||
error_domain = error.code().domain(),
|
||||
error_code = error.code().code(),
|
||||
"Raw Transaction Ingest Desk shared Store shutdown failed after final Worker"
|
||||
);
|
||||
}
|
||||
self.runtime_state.complete_store_close(self.token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts one exact route Worker and reuses the same ready Store for any already-active same-network route.
|
||||
pub(crate) async fn start_route_runtime(
|
||||
runtime_state: std::sync::Arc<crate::RouteRuntimeState>,
|
||||
prepared: crate::PreparedRouteStart,
|
||||
) -> ksp_core_lib::Result<crate::RouteRuntimeLaunch> {
|
||||
let reservation = match runtime_state.reserve(&prepared) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let store = if reservation.needs_store_open {
|
||||
let store = ksp_store_lib::Store::open(prepared.store_settings).await;
|
||||
let store = match store {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
let _rollback = runtime_state.rollback(reservation.token);
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_RUNTIME_START_FAILED, "Cannot open Raw Transaction Ingest Desk shared Store runtime")
|
||||
.with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
let health = store.health().await;
|
||||
if health.state() != ksp_store_lib::StoreHealthState::Ready {
|
||||
let close = store.close().await;
|
||||
let _rollback = runtime_state.rollback(reservation.token);
|
||||
if let std::result::Result::Err(error) = close {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED,
|
||||
"Cannot close non-ready Raw Transaction Ingest Desk shared Store runtime",
|
||||
)
|
||||
.with_source(error),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_RUNTIME_STORE_NOT_READY,
|
||||
"Raw Transaction Ingest Desk shared Store readiness probe did not prove Ready",
|
||||
));
|
||||
}
|
||||
let store = std::sync::Arc::new(store);
|
||||
if let std::result::Result::Err(error) = runtime_state.install_store(reservation.token, std::sync::Arc::clone(&store)) {
|
||||
let close = close_store_arc(store).await;
|
||||
let _rollback = runtime_state.rollback(reservation.token);
|
||||
if let std::result::Result::Err(close_error) = close {
|
||||
return std::result::Result::Err(close_error);
|
||||
}
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
store
|
||||
} else {
|
||||
std::mem::drop(prepared.store_settings);
|
||||
match runtime_state.shared_store(prepared.network.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
let _rollback = runtime_state.rollback(reservation.token);
|
||||
return std::result::Result::Err(error);
|
||||
},
|
||||
}
|
||||
};
|
||||
let network = store.runtime_snapshot().network().clone();
|
||||
let settings = ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSettings::with_defaults(network, reservation.worker_id.clone());
|
||||
let handle = ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestWorker::start_with_runtime_resources(
|
||||
settings,
|
||||
std::sync::Arc::clone(&store),
|
||||
prepared.runtime_resources,
|
||||
);
|
||||
let handle = match handle {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
let cleanup_store = runtime_state.rollback(reservation.token);
|
||||
let cleanup_store = match cleanup_store {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(rollback_error) => return std::result::Result::Err(rollback_error),
|
||||
};
|
||||
std::mem::drop(store);
|
||||
if let std::option::Option::Some(value) = cleanup_store {
|
||||
let close = close_store_arc(value).await;
|
||||
runtime_state.complete_store_close(reservation.token);
|
||||
if let std::result::Result::Err(close_error) = close {
|
||||
return std::result::Result::Err(close_error);
|
||||
}
|
||||
}
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_RUNTIME_START_FAILED, "Cannot start Raw Transaction Ingest Desk route Worker runtime")
|
||||
.with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
let acknowledgement = runtime_state.activate(&reservation, handle.clone());
|
||||
let acknowledgement = match acknowledgement {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
let _stop_requested = handle.request_stop();
|
||||
let _terminal = handle.wait_terminal().await;
|
||||
let cleanup_store = runtime_state.rollback(reservation.token);
|
||||
let cleanup_store = match cleanup_store {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(rollback_error) => return std::result::Result::Err(rollback_error),
|
||||
};
|
||||
std::mem::drop(store);
|
||||
if let std::option::Option::Some(value) = cleanup_store {
|
||||
let close = close_store_arc(value).await;
|
||||
runtime_state.complete_store_close(reservation.token);
|
||||
if let std::result::Result::Err(close_error) = close {
|
||||
return std::result::Result::Err(close_error);
|
||||
}
|
||||
}
|
||||
return std::result::Result::Err(error);
|
||||
},
|
||||
};
|
||||
std::mem::drop(store);
|
||||
ksp_logging_lib::info!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
|
||||
profile_id = reservation.identity.profile_id.as_str(),
|
||||
network = reservation.identity.network.as_str(),
|
||||
route_id = reservation.identity.route_id.as_str(),
|
||||
commitment = reservation.identity.commitment.as_str(),
|
||||
"Raw Transaction Ingest Desk independent route Worker handle installed on shared Store"
|
||||
);
|
||||
return std::result::Result::Ok(crate::RouteRuntimeLaunch { acknowledgement, handle, runtime_state, token: reservation.token });
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RouteRuntimeIdentity {
|
||||
commitment: crate::RawIngestCommitment,
|
||||
inventory_generation: u32,
|
||||
network: String,
|
||||
profile_id: String,
|
||||
route_id: crate::RawIngestRouteId,
|
||||
}
|
||||
|
||||
impl RouteRuntimeIdentity {
|
||||
fn dto(&self, state: crate::RawIngestRouteState) -> crate::RawIngestRouteRuntimeDto {
|
||||
return crate::RawIngestRouteRuntimeDto {
|
||||
commitment: self.commitment,
|
||||
inventory_generation: self.inventory_generation,
|
||||
network: self.network.clone(),
|
||||
profile_id: self.profile_id.clone(),
|
||||
route_id: self.route_id,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
fn matches_dto(&self, value: &crate::RawIngestRouteRuntimeDto) -> bool {
|
||||
return self.profile_id == value.profile_id && self.route_id == value.route_id;
|
||||
}
|
||||
|
||||
fn matches_monitoring(&self, value: &crate::RawIngestRouteMonitoringDto) -> bool {
|
||||
return self.profile_id == value.profile_id && self.route_id == value.route_id;
|
||||
}
|
||||
}
|
||||
|
||||
enum RouteStopTarget {
|
||||
Active {
|
||||
handle: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHandle,
|
||||
identity: RouteRuntimeIdentity,
|
||||
token: u64,
|
||||
},
|
||||
Starting {
|
||||
identity: RouteRuntimeIdentity,
|
||||
token: u64,
|
||||
},
|
||||
Terminal(crate::RawIngestRouteRuntimeDto),
|
||||
TerminalClosing,
|
||||
}
|
||||
|
||||
struct RouteRuntimeReservation {
|
||||
identity: RouteRuntimeIdentity,
|
||||
needs_store_open: bool,
|
||||
token: u64,
|
||||
worker_id: ksp_worker_api::WorkerId,
|
||||
}
|
||||
|
||||
enum RouteRuntimeSlot {
|
||||
Starting {
|
||||
identity: RouteRuntimeIdentity,
|
||||
stop_requested: bool,
|
||||
token: u64,
|
||||
},
|
||||
Active {
|
||||
handle: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHandle,
|
||||
identity: RouteRuntimeIdentity,
|
||||
token: u64,
|
||||
},
|
||||
}
|
||||
|
||||
impl RouteRuntimeSlot {
|
||||
fn matches_identity(&self, profile_id: &str, route_id: crate::RawIngestRouteId) -> bool {
|
||||
let identity = match self {
|
||||
Self::Starting { identity, .. } | Self::Active { identity, .. } => identity,
|
||||
};
|
||||
return identity.profile_id == profile_id && identity.route_id == route_id;
|
||||
}
|
||||
|
||||
fn starting_token(&self) -> std::option::Option<u64> {
|
||||
return match self {
|
||||
Self::Starting { token, .. } => std::option::Option::Some(*token),
|
||||
Self::Active { .. } => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
fn cancelled_start_terminal(&self) -> std::option::Option<crate::RawIngestRouteRuntimeDto> {
|
||||
return match self {
|
||||
Self::Starting { identity, stop_requested: true, .. } => std::option::Option::Some(identity.dto(crate::RawIngestRouteState::Stopped)),
|
||||
Self::Starting { stop_requested: false, .. } | Self::Active { .. } => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
fn token(&self) -> u64 {
|
||||
return match self {
|
||||
Self::Starting { token, .. } | Self::Active { token, .. } => *token,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async fn close_store_arc(store: std::sync::Arc<ksp_store_lib::Store>) -> ksp_core_lib::Result<()> {
|
||||
let started = std::time::Instant::now();
|
||||
let mut store = store;
|
||||
loop {
|
||||
match std::sync::Arc::try_unwrap(store) {
|
||||
std::result::Result::Ok(value) => {
|
||||
let close = value.close().await;
|
||||
return match close {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED,
|
||||
"Cannot close Raw Transaction Ingest Desk shared Store runtime",
|
||||
)
|
||||
.with_source(error),
|
||||
),
|
||||
};
|
||||
},
|
||||
std::result::Result::Err(shared) => store = shared,
|
||||
}
|
||||
if started.elapsed() >= STORE_RECLAIM_TIMEOUT {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED,
|
||||
"Raw Transaction Ingest Desk shared Store remained referenced after final Worker",
|
||||
));
|
||||
}
|
||||
tokio::time::sleep(STORE_RECLAIM_POLL_INTERVAL).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn project_worker_state(state: ksp_worker_api::WorkerState) -> crate::RawIngestRouteState {
|
||||
return match state {
|
||||
ksp_worker_api::WorkerState::Created | ksp_worker_api::WorkerState::Starting => crate::RawIngestRouteState::Starting,
|
||||
ksp_worker_api::WorkerState::Running => crate::RawIngestRouteState::Running,
|
||||
ksp_worker_api::WorkerState::Stopping => crate::RawIngestRouteState::Stopping,
|
||||
ksp_worker_api::WorkerState::Stopped => crate::RawIngestRouteState::Stopped,
|
||||
ksp_worker_api::WorkerState::Faulted(_) => crate::RawIngestRouteState::Faulted,
|
||||
_ => crate::RawIngestRouteState::Faulted,
|
||||
};
|
||||
}
|
||||
|
||||
fn runtime_lock_error() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_LOCK_FAILED, "Raw Transaction Ingest Desk multi-route runtime state lock is poisoned");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/route_runtime.rs"]
|
||||
mod tests;
|
||||
518
crates/ksp-app-raw-transaction-ingest-desk/src/route_start.rs
Normal file
518
crates/ksp-app-raw-transaction-ingest-desk/src/route_start.rs
Normal file
@@ -0,0 +1,518 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_start.rs
|
||||
// version: 5
|
||||
|
||||
//! Start-time Config revalidation and exact Transport/Store resource reconstruction for route validation and runtime launch.
|
||||
|
||||
/// Fully revalidated Start preparation retaining backend-only Store settings and exact Worker runtime resources.
|
||||
pub(crate) struct PreparedRouteStart {
|
||||
/// Confirmed/finalized commitment validated by the exact route constructor.
|
||||
pub(crate) commitment: crate::RawIngestCommitment,
|
||||
/// Inventory generation proven current while this preparation was built.
|
||||
pub(crate) inventory_generation: u32,
|
||||
/// Logical network proven identical across Store and Transport.
|
||||
pub(crate) network: String,
|
||||
/// Logical network-profile identifier selected by the operator.
|
||||
pub(crate) profile_id: String,
|
||||
/// Logical route selected by the operator.
|
||||
pub(crate) route_id: crate::RawIngestRouteId,
|
||||
/// Backend-neutral Store settings resolved from Config and retained only in Rust.
|
||||
pub(crate) store_settings: ksp_store_lib::StoreSettings,
|
||||
/// Exact single-route Worker runtime resources reconstructed from typed Transport contracts.
|
||||
pub(crate) runtime_resources: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources,
|
||||
}
|
||||
|
||||
/// Revalidates one Start request and retains the exact backend resources required for a later Worker launch.
|
||||
pub(crate) fn prepare_route_start(
|
||||
management: &ksp_config_lib::ConfigManagement,
|
||||
current_generation: u32,
|
||||
request: &crate::RawIngestRouteStartRequestDto,
|
||||
) -> ksp_core_lib::Result<crate::PreparedRouteStart> {
|
||||
let identity = request.validate_logical_identity();
|
||||
if let std::result::Result::Err(error) = identity {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if current_generation == 0 || request.inventory_generation != current_generation {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_START_STALE_INVENTORY,
|
||||
"Raw Transaction Ingest Desk Start request uses a stale route inventory generation",
|
||||
));
|
||||
}
|
||||
let environment = ksp_config_lib::ConfigEnvironment::load();
|
||||
let environment = match environment {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return route_start_error("Cannot resolve Start-time Config environment", error),
|
||||
};
|
||||
let inventory = crate::build_route_inventory_with_environment(management, &environment, current_generation);
|
||||
let inventory = match inventory {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return route_start_error("Cannot rebuild Start-time route inventory", error),
|
||||
};
|
||||
let profile = inventory.profiles.iter().find(|profile| return profile.profile_id == request.profile_id);
|
||||
let profile = match profile {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return route_unavailable_error(),
|
||||
};
|
||||
let route = profile.routes.iter().find(|route| return route.route_id == request.route_id);
|
||||
let route = match route {
|
||||
std::option::Option::Some(value) if value.selectable && value.state == crate::RawIngestRouteState::Configured => value,
|
||||
std::option::Option::Some(_) | std::option::Option::None => return route_unavailable_error(),
|
||||
};
|
||||
let network = match route.network.as_deref() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return route_unavailable_error(),
|
||||
};
|
||||
let composite = crate::load_raw_transaction_ingest_desk_composite_profile(management, std::option::Option::Some(request.profile_id.as_str()));
|
||||
let composite = match composite {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return route_start_error("Cannot resolve Start-time composite profile", error),
|
||||
};
|
||||
let store_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_STORE, ksp_config_lib::FILE_ID_STD_STORE);
|
||||
let store_profile = match store_profile {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return route_start_error("Cannot resolve Start-time Store profile", error),
|
||||
};
|
||||
let store = management.engine().resolve_store_config_profile(&store_profile, &environment);
|
||||
let store = match store {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return route_start_error("Cannot resolve Start-time Store settings", error),
|
||||
};
|
||||
if store.settings().network().as_str() != network {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED,
|
||||
"Raw Transaction Ingest Desk Start-time Store network no longer matches the selected route",
|
||||
));
|
||||
}
|
||||
let transport_profile =
|
||||
crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT, ksp_config_lib::FILE_ID_STD_TRANSPORT);
|
||||
let transport_profile = match transport_profile {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return route_start_error("Cannot resolve Start-time base Transport profile", error),
|
||||
};
|
||||
let transport = management.engine().resolve_transport_config_profile(&transport_profile, &environment);
|
||||
let transport = match transport {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return route_start_error("Cannot resolve Start-time base Transport settings", error),
|
||||
};
|
||||
if !transport_matches_network(&transport, network) {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED,
|
||||
"Raw Transaction Ingest Desk Start-time Transport network no longer matches the selected route",
|
||||
));
|
||||
}
|
||||
let commitment = request.commitment.into_transport();
|
||||
let runtime_resources = match request.route_id {
|
||||
crate::RawIngestRouteId::YellowstoneHydrated => prepare_yellowstone(management, &environment, &composite, network, commitment),
|
||||
crate::RawIngestRouteId::StandardLogsHydrated => prepare_standard_logs(&transport, network, commitment),
|
||||
crate::RawIngestRouteId::StandardBlockDirect => prepare_standard_block(&transport, network, commitment),
|
||||
crate::RawIngestRouteId::HeliusTransactionHydrated => prepare_helius(management, &environment, &composite, &transport, network, commitment),
|
||||
crate::RawIngestRouteId::HttpBlockPolling => prepare_http_polling(&transport, network, commitment),
|
||||
};
|
||||
let runtime_resources = match runtime_resources {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return route_start_error("Cannot reconstruct Start-time Transport resources", error),
|
||||
};
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_ROUTE_START,
|
||||
profile_id = request.profile_id.as_str(),
|
||||
network,
|
||||
route_id = request.route_id.as_str(),
|
||||
inventory_generation = request.inventory_generation,
|
||||
commitment = request.commitment.as_str(),
|
||||
"revalidated Start request and reconstructed exact Store/Transport-owned Worker launch resources"
|
||||
);
|
||||
return std::result::Result::Ok(crate::PreparedRouteStart {
|
||||
commitment: request.commitment,
|
||||
inventory_generation: request.inventory_generation,
|
||||
network: network.to_owned(),
|
||||
profile_id: request.profile_id.clone(),
|
||||
route_id: request.route_id,
|
||||
store_settings: store.into_settings(),
|
||||
runtime_resources,
|
||||
});
|
||||
}
|
||||
|
||||
/// Revalidates one future Start request and destroys the reconstructed resources without opening Store or launching a Worker.
|
||||
pub(crate) fn validate_route_start(
|
||||
management: &ksp_config_lib::ConfigManagement,
|
||||
current_generation: u32,
|
||||
request: &crate::RawIngestRouteStartRequestDto,
|
||||
) -> ksp_core_lib::Result<crate::RawIngestRouteStartValidationDto> {
|
||||
let prepared = crate::prepare_route_start(management, current_generation, request);
|
||||
let prepared = match prepared {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::RawIngestRouteStartValidationDto {
|
||||
commitment: prepared.commitment,
|
||||
inventory_generation: prepared.inventory_generation,
|
||||
network: prepared.network,
|
||||
profile_id: prepared.profile_id,
|
||||
route_id: prepared.route_id,
|
||||
});
|
||||
}
|
||||
|
||||
fn prepare_standard_logs(
|
||||
transport: &ksp_config_lib::ResolvedTransportConfig,
|
||||
network: &str,
|
||||
commitment: ksp_onchain_transport_lib::SolanaCommitment,
|
||||
) -> ksp_core_lib::Result<ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources> {
|
||||
let endpoint =
|
||||
single_ws_endpoint(transport, network, ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard, ksp_onchain_transport_lib::WsSubscriptionKind::Logs);
|
||||
let endpoint = match endpoint {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let role = single_http_role(transport.http_settings(), network, &["getTransaction"]);
|
||||
let role = match role {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let pool = ksp_onchain_transport_lib::HttpTransportPool::new(transport.http_settings().clone());
|
||||
let pool = match pool {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let source = ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestStandardLogsSource::new(
|
||||
endpoint,
|
||||
ksp_onchain_transport_lib::SolanaLogsSubscribeFilter::AllWithVotes,
|
||||
commitment,
|
||||
pool,
|
||||
role,
|
||||
);
|
||||
let source = match source {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources::from_standard_logs_source(source));
|
||||
}
|
||||
|
||||
fn prepare_standard_block(
|
||||
transport: &ksp_config_lib::ResolvedTransportConfig,
|
||||
network: &str,
|
||||
commitment: ksp_onchain_transport_lib::SolanaCommitment,
|
||||
) -> ksp_core_lib::Result<ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources> {
|
||||
let endpoint =
|
||||
single_ws_endpoint(transport, network, ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard, ksp_onchain_transport_lib::WsSubscriptionKind::Block);
|
||||
let endpoint = match endpoint {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let source = ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestStandardBlockSource::new(
|
||||
endpoint,
|
||||
ksp_onchain_transport_lib::SolanaBlockSubscribeFilter::All,
|
||||
commitment,
|
||||
);
|
||||
let source = match source {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources::from_standard_block_source(source));
|
||||
}
|
||||
|
||||
fn prepare_helius(
|
||||
management: &ksp_config_lib::ConfigManagement,
|
||||
environment: &ksp_config_lib::ConfigEnvironment,
|
||||
composite: &ksp_config_lib::ResolvedConfigComposite,
|
||||
base_transport: &ksp_config_lib::ResolvedTransportConfig,
|
||||
network: &str,
|
||||
commitment: ksp_onchain_transport_lib::SolanaCommitment,
|
||||
) -> ksp_core_lib::Result<ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources> {
|
||||
let transport = resolve_optional_transport(management, environment, composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT_HELIUS, network);
|
||||
let transport = match transport {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let endpoint = single_ws_endpoint(
|
||||
&transport,
|
||||
network,
|
||||
ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream,
|
||||
ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction,
|
||||
);
|
||||
let endpoint = match endpoint {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let role = single_http_role(base_transport.http_settings(), network, &["getTransaction"]);
|
||||
let role = match role {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let pool = ksp_onchain_transport_lib::HttpTransportPool::new(base_transport.http_settings().clone());
|
||||
let pool = match pool {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let filter = ksp_onchain_transport_lib::HeliusTransactionSubscribeFilter::new(
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
);
|
||||
let source = ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHeliusTransactionSource::new(endpoint, filter, commitment, pool, role);
|
||||
let source = match source {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources::from_helius_transaction_source(source));
|
||||
}
|
||||
|
||||
fn prepare_yellowstone(
|
||||
management: &ksp_config_lib::ConfigManagement,
|
||||
environment: &ksp_config_lib::ConfigEnvironment,
|
||||
composite: &ksp_config_lib::ResolvedConfigComposite,
|
||||
network: &str,
|
||||
commitment: ksp_onchain_transport_lib::SolanaCommitment,
|
||||
) -> ksp_core_lib::Result<ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources> {
|
||||
let transport = resolve_optional_transport(management, environment, composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT_YELLOWSTONE, network);
|
||||
let transport = match transport {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let endpoint = single_grpc_endpoint(&transport, network);
|
||||
let endpoint = match endpoint {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let channel = ksp_onchain_transport_lib::YellowstoneGrpcChannel::prepare(&endpoint);
|
||||
let channel = match channel {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let filter_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("raw-ingest-blocks");
|
||||
let filter_name = match filter_name {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut block_filter = ksp_onchain_transport_lib::YellowstoneSubscribeBlockFilter::new();
|
||||
block_filter.set_include_transactions(std::option::Option::Some(false));
|
||||
block_filter.set_include_accounts(std::option::Option::Some(false));
|
||||
block_filter.set_include_entries(std::option::Option::Some(false));
|
||||
let mut request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new();
|
||||
let inserted = request.insert_block_filter(filter_name, block_filter);
|
||||
if let std::result::Result::Err(error) = inserted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
request.set_commitment(std::option::Option::Some(commitment));
|
||||
let role = single_http_role(transport.http_settings(), network, &["getBlock"]);
|
||||
let role = match role {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let pool = ksp_onchain_transport_lib::HttpTransportPool::new(transport.http_settings().clone());
|
||||
let pool = match pool {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let source = ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestYellowstoneSource::new(channel, request, pool, role);
|
||||
let source = match source {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources::new(source));
|
||||
}
|
||||
|
||||
fn prepare_http_polling(
|
||||
transport: &ksp_config_lib::ResolvedTransportConfig,
|
||||
network: &str,
|
||||
commitment: ksp_onchain_transport_lib::SolanaCommitment,
|
||||
) -> ksp_core_lib::Result<ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources> {
|
||||
let role = single_http_role(transport.http_settings(), network, &["getSlot", "getBlocksWithLimit", "getBlock"]);
|
||||
let role = match role {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let pool = ksp_onchain_transport_lib::HttpTransportPool::new(transport.http_settings().clone());
|
||||
let pool = match pool {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let source = ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHttpBlockPollingSource::new(pool, role, commitment);
|
||||
let source = match source {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources::from_http_block_polling_source(source));
|
||||
}
|
||||
|
||||
fn resolve_optional_transport(
|
||||
management: &ksp_config_lib::ConfigManagement,
|
||||
environment: &ksp_config_lib::ConfigEnvironment,
|
||||
composite: &ksp_config_lib::ResolvedConfigComposite,
|
||||
component_id: &str,
|
||||
network: &str,
|
||||
) -> ksp_core_lib::Result<ksp_config_lib::ResolvedTransportConfig> {
|
||||
let component = composite.component(component_id);
|
||||
let component = match component {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED,
|
||||
"Raw Transaction Ingest Desk selected route has no declared Transport source",
|
||||
));
|
||||
},
|
||||
};
|
||||
if component.resolved().file_id().as_str() != ksp_config_lib::FILE_ID_STD_TRANSPORT {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED,
|
||||
"Raw Transaction Ingest Desk selected route references an unexpected Transport document",
|
||||
));
|
||||
}
|
||||
let resolved = management.engine().resolve_transport_config_profile(component.resolved(), environment);
|
||||
let resolved = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if !transport_matches_network(&resolved, network) {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED,
|
||||
"Raw Transaction Ingest Desk provider Transport network no longer matches the selected route",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(resolved);
|
||||
}
|
||||
|
||||
fn single_ws_endpoint(
|
||||
transport: &ksp_config_lib::ResolvedTransportConfig,
|
||||
network: &str,
|
||||
protocol: ksp_onchain_transport_lib::WsProtocolKind,
|
||||
capability: ksp_onchain_transport_lib::WsSubscriptionKind,
|
||||
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::WsEndpointSettings> {
|
||||
let settings = match transport.ws_settings() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return ambiguous_resource_error("Start-time route requires one WebSocket endpoint but none is configured"),
|
||||
};
|
||||
let matching = settings
|
||||
.endpoints()
|
||||
.iter()
|
||||
.filter(|endpoint| {
|
||||
return endpoint.enabled()
|
||||
&& endpoint.cluster().as_str() == network
|
||||
&& endpoint.protocol() == protocol
|
||||
&& endpoint.supports_subscription(capability);
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
if matching.len() != 1 {
|
||||
return ambiguous_resource_error("Start-time route requires exactly one matching WebSocket endpoint in its selected Transport profile");
|
||||
}
|
||||
return std::result::Result::Ok(matching[0].clone());
|
||||
}
|
||||
|
||||
fn single_grpc_endpoint(
|
||||
transport: &ksp_config_lib::ResolvedTransportConfig,
|
||||
network: &str,
|
||||
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings> {
|
||||
let settings = match transport.grpc_settings() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return ambiguous_resource_error("Start-time route requires one Yellowstone gRPC endpoint but none is configured"),
|
||||
};
|
||||
let matching = settings
|
||||
.endpoints()
|
||||
.iter()
|
||||
.filter(|endpoint| return endpoint.enabled() && endpoint.cluster().as_str() == network)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
if matching.len() != 1 {
|
||||
return ambiguous_resource_error("Start-time route requires exactly one matching Yellowstone gRPC endpoint in its selected Transport profile");
|
||||
}
|
||||
return std::result::Result::Ok(matching[0].clone());
|
||||
}
|
||||
|
||||
fn single_http_role(
|
||||
settings: &ksp_onchain_transport_lib::HttpTransportSettings,
|
||||
network: &str,
|
||||
methods: &[&'static str],
|
||||
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::HttpRoleName> {
|
||||
let mut request_kinds = std::vec::Vec::<&'static str>::with_capacity(methods.len());
|
||||
for method_name in methods {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name);
|
||||
let descriptor = match descriptor {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return ambiguous_resource_error("Start-time route references an unknown HTTP method requirement"),
|
||||
};
|
||||
request_kinds.push(descriptor.request_kind());
|
||||
}
|
||||
let mut roles = std::collections::BTreeSet::<String>::new();
|
||||
for endpoint in settings.endpoints() {
|
||||
if !endpoint.enabled() || endpoint.cluster().as_str() != network {
|
||||
continue;
|
||||
}
|
||||
for role in endpoint.roles() {
|
||||
if role.enabled() {
|
||||
roles.insert(role.role().as_str().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
let matching = roles
|
||||
.into_iter()
|
||||
.filter(|role_name| {
|
||||
return request_kinds.iter().all(|request_kind| return http_role_supports_request_kind(settings, network, role_name.as_str(), request_kind));
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
if matching.len() != 1 {
|
||||
return ambiguous_resource_error("Start-time route requires exactly one logical HTTP role satisfying its complete method set");
|
||||
}
|
||||
return std::result::Result::Ok(ksp_onchain_transport_lib::HttpRoleName::new(matching[0].clone()));
|
||||
}
|
||||
|
||||
fn http_role_supports_request_kind(settings: &ksp_onchain_transport_lib::HttpTransportSettings, network: &str, role_name: &str, request_kind: &str) -> bool {
|
||||
for endpoint in settings.endpoints() {
|
||||
if !endpoint.enabled() || endpoint.cluster().as_str() != network {
|
||||
continue;
|
||||
}
|
||||
for role in endpoint.roles() {
|
||||
if !role.enabled() || role.role().as_str() != role_name {
|
||||
continue;
|
||||
}
|
||||
if role.request_kinds().iter().any(|kind| return kind.is_wildcard() || kind.as_str() == request_kind) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn transport_matches_network(transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) -> bool {
|
||||
let mut networks = std::collections::BTreeSet::<String>::new();
|
||||
for endpoint in transport.http_settings().endpoints() {
|
||||
if endpoint.enabled() {
|
||||
networks.insert(endpoint.cluster().as_str().to_owned());
|
||||
}
|
||||
}
|
||||
if let std::option::Option::Some(settings) = transport.ws_settings() {
|
||||
for endpoint in settings.endpoints() {
|
||||
if endpoint.enabled() {
|
||||
networks.insert(endpoint.cluster().as_str().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let std::option::Option::Some(settings) = transport.grpc_settings() {
|
||||
for endpoint in settings.endpoints() {
|
||||
if endpoint.enabled() {
|
||||
networks.insert(endpoint.cluster().as_str().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
return networks.len() == 1 && networks.contains(network);
|
||||
}
|
||||
|
||||
fn route_unavailable_error<T>() -> ksp_core_lib::Result<T> {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_START_UNAVAILABLE,
|
||||
"Raw Transaction Ingest Desk selected route is no longer Configured in the current Start-time inventory",
|
||||
));
|
||||
}
|
||||
|
||||
fn ambiguous_resource_error<T>(message: &'static str) -> ksp_core_lib::Result<T> {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED, message));
|
||||
}
|
||||
|
||||
fn route_start_error<T>(message: &'static str, source: ksp_core_lib::Error) -> ksp_core_lib::Result<T> {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED, message).with_source(source));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/route_start.rs"]
|
||||
mod tests;
|
||||
214
crates/ksp-app-raw-transaction-ingest-desk/src/splash.rs
Normal file
214
crates/ksp-app-raw-transaction-ingest-desk/src/splash.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/splash.rs
|
||||
// version: 1
|
||||
|
||||
//! Common splash settings and frontend event contracts for Raw Transaction Ingest Desk.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
const DEFAULT_SPLASH_FADE_IN_MS: u32 = 300;
|
||||
const DEFAULT_SPLASH_FADE_OUT_MS: u32 = 300;
|
||||
const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200;
|
||||
const ENV_SPLASH_FADE_IN_MS: &str = "KSP_DESK_SPLASH_FADE_IN_MS";
|
||||
const ENV_SPLASH_FADE_OUT_MS: &str = "KSP_DESK_SPLASH_FADE_OUT_MS";
|
||||
const ENV_SPLASH_MINIMUM_MS: &str = "KSP_DESK_SPLASH_MINIMUM_MS";
|
||||
const MAX_SPLASH_FADE_MS: u32 = 10_000;
|
||||
const MAX_SPLASH_MINIMUM_MS: u64 = 60_000;
|
||||
|
||||
/// Runtime timings used by the common desk splash lifecycle.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct SplashSettings {
|
||||
fade_in_ms: u32,
|
||||
minimum_ms: u64,
|
||||
fade_out_ms: u32,
|
||||
fade_in_source: ksp_config_lib::ConfigEnvironmentSource,
|
||||
minimum_source: ksp_config_lib::ConfigEnvironmentSource,
|
||||
fade_out_source: ksp_config_lib::ConfigEnvironmentSource,
|
||||
}
|
||||
|
||||
impl SplashSettings {
|
||||
/// Resolves splash timings through the Config-owned environment snapshot.
|
||||
pub(crate) fn load() -> ksp_core_lib::Result<Self> {
|
||||
let environment = ksp_config_lib::ConfigEnvironment::load();
|
||||
let environment = match environment {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let fade_in = environment.resolve_variable(ENV_SPLASH_FADE_IN_MS, std::option::Option::Some("300"));
|
||||
let fade_in = match fade_in {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let minimum = environment.resolve_variable(ENV_SPLASH_MINIMUM_MS, std::option::Option::Some("1200"));
|
||||
let minimum = match minimum {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let fade_out = environment.resolve_variable(ENV_SPLASH_FADE_OUT_MS, std::option::Option::Some("300"));
|
||||
let fade_out = match fade_out {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let fade_in_ms = parse_u32_setting(ENV_SPLASH_FADE_IN_MS, fade_in.value(), MAX_SPLASH_FADE_MS);
|
||||
let fade_in_ms = match fade_in_ms {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let minimum_ms = parse_u64_setting(ENV_SPLASH_MINIMUM_MS, minimum.value(), MAX_SPLASH_MINIMUM_MS);
|
||||
let minimum_ms = match minimum_ms {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let fade_out_ms = parse_u32_setting(ENV_SPLASH_FADE_OUT_MS, fade_out.value(), MAX_SPLASH_FADE_MS);
|
||||
let fade_out_ms = match fade_out_ms {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(Self {
|
||||
fade_in_ms,
|
||||
minimum_ms,
|
||||
fade_out_ms,
|
||||
fade_in_source: fade_in.source(),
|
||||
minimum_source: minimum.source(),
|
||||
fade_out_source: fade_out.source(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns safe in-memory timings used when the managed environment cannot be resolved.
|
||||
#[must_use]
|
||||
pub(crate) const fn fallback() -> Self {
|
||||
return Self {
|
||||
fade_in_ms: DEFAULT_SPLASH_FADE_IN_MS,
|
||||
minimum_ms: DEFAULT_SPLASH_MINIMUM_MS,
|
||||
fade_out_ms: DEFAULT_SPLASH_FADE_OUT_MS,
|
||||
fade_in_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
|
||||
minimum_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
|
||||
fade_out_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the configured fade-in duration.
|
||||
#[must_use]
|
||||
pub(crate) const fn fade_in_ms(self) -> u32 {
|
||||
return self.fade_in_ms;
|
||||
}
|
||||
|
||||
/// Returns the minimum fully-visible splash duration.
|
||||
#[must_use]
|
||||
pub(crate) const fn minimum_ms(self) -> u64 {
|
||||
return self.minimum_ms;
|
||||
}
|
||||
|
||||
/// Returns the configured fade-out duration.
|
||||
#[must_use]
|
||||
pub(crate) const fn fade_out_ms(self) -> u32 {
|
||||
return self.fade_out_ms;
|
||||
}
|
||||
|
||||
/// Returns the safe provenance code for the fade-in duration.
|
||||
#[must_use]
|
||||
pub(crate) const fn fade_in_source(self) -> &'static str {
|
||||
return environment_source_code(self.fade_in_source);
|
||||
}
|
||||
|
||||
/// Returns the safe provenance code for the minimum duration.
|
||||
#[must_use]
|
||||
pub(crate) const fn minimum_source(self) -> &'static str {
|
||||
return environment_source_code(self.minimum_source);
|
||||
}
|
||||
|
||||
/// Returns the safe provenance code for the fade-out duration.
|
||||
#[must_use]
|
||||
pub(crate) const fn fade_out_source(self) -> &'static str {
|
||||
return environment_source_code(self.fade_out_source);
|
||||
}
|
||||
|
||||
/// Returns the expected additive backend lifecycle from readiness through fade-out.
|
||||
#[must_use]
|
||||
pub(crate) fn expected_backend_lifecycle_ms(self) -> u64 {
|
||||
return u64::from(self.fade_in_ms) + self.minimum_ms + u64::from(self.fade_out_ms);
|
||||
}
|
||||
}
|
||||
|
||||
/// Command emitted by Rust to the splash frontend.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/splash/SplashOrderDto.ts")]
|
||||
pub(crate) struct SplashOrderDto {
|
||||
/// Splash action name.
|
||||
pub(crate) action: String,
|
||||
/// Optional status text.
|
||||
pub(crate) message: std::option::Option<String>,
|
||||
/// Optional message severity used by the bottom message feed.
|
||||
pub(crate) status: std::option::Option<String>,
|
||||
/// Optional animation duration in milliseconds.
|
||||
pub(crate) duration_ms: std::option::Option<u32>,
|
||||
}
|
||||
|
||||
impl SplashOrderDto {
|
||||
/// Creates a new `SplashOrderDto` value.
|
||||
#[must_use]
|
||||
pub(crate) fn new(action: &str, message: std::option::Option<&str>, status: std::option::Option<&str>, duration_ms: std::option::Option<u32>) -> Self {
|
||||
return Self {
|
||||
action: action.to_owned(),
|
||||
message: message.map(std::string::ToString::to_string),
|
||||
status: status.map(std::string::ToString::to_string),
|
||||
duration_ms,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const fn environment_source_code(source: ksp_config_lib::ConfigEnvironmentSource) -> &'static str {
|
||||
return match source {
|
||||
ksp_config_lib::ConfigEnvironmentSource::Process => "process",
|
||||
ksp_config_lib::ConfigEnvironmentSource::DotEnv => "dotenv",
|
||||
ksp_config_lib::ConfigEnvironmentSource::Fallback => "fallback",
|
||||
};
|
||||
}
|
||||
|
||||
fn parse_u64_setting(variable_name: &str, value: &str, maximum: u64) -> ksp_core_lib::Result<u64> {
|
||||
let parsed = value.parse::<u64>();
|
||||
let parsed = match parsed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration is not a valid unsigned integer")
|
||||
.with_context("variable_name", variable_name)
|
||||
.with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
if parsed > maximum {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration exceeds the allowed bound")
|
||||
.with_context("variable_name", variable_name)
|
||||
.with_context("maximum_ms", maximum.to_string()),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(parsed);
|
||||
}
|
||||
|
||||
fn parse_u32_setting(variable_name: &str, value: &str, maximum: u32) -> ksp_core_lib::Result<u32> {
|
||||
let parsed = value.parse::<u32>();
|
||||
let parsed = match parsed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration is not a valid unsigned integer")
|
||||
.with_context("variable_name", variable_name)
|
||||
.with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
if parsed > maximum {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration exceeds the allowed bound")
|
||||
.with_context("variable_name", variable_name)
|
||||
.with_context("maximum_ms", maximum.to_string()),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(parsed);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/splash.rs"]
|
||||
mod tests;
|
||||
318
crates/ksp-app-raw-transaction-ingest-desk/src/tauri.rs
Normal file
318
crates/ksp-app-raw-transaction-ingest-desk/src/tauri.rs
Normal file
@@ -0,0 +1,318 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/tauri.rs
|
||||
// version: 9
|
||||
|
||||
//! Tauri runtime assembly for the KSP Raw Transaction Ingest desktop application.
|
||||
|
||||
use tauri::Emitter; // rust-rules: trait-import
|
||||
use tauri::Manager; // rust-rules: trait-import
|
||||
|
||||
/// Runs the Raw Transaction Ingest desktop application.
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
|
||||
let context = tauri::generate_context!();
|
||||
let runtime_layout = configure_packaged_runtime(&context);
|
||||
if let std::result::Result::Err(error) = runtime_layout {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let app_state = crate::AppState::initialize(arguments);
|
||||
let app_state = match app_state {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut builder = tauri::Builder::default();
|
||||
builder = builder.manage(app_state);
|
||||
builder = builder.plugin(tauri_plugin_tracing::Builder::new().build::<tauri::Wry>());
|
||||
builder = configure_commands(builder);
|
||||
builder = configure_window_events(builder);
|
||||
builder = builder.setup(|app| {
|
||||
let splash = crate::require_splash_window(app);
|
||||
if let std::result::Result::Err(error) = splash {
|
||||
return std::result::Result::Err(std::boxed::Box::new(error));
|
||||
}
|
||||
let main = crate::require_main_window(app);
|
||||
if let std::result::Result::Err(error) = main {
|
||||
return std::result::Result::Err(std::boxed::Box::new(error));
|
||||
}
|
||||
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_SHELL, "Raw Transaction Ingest Desk Tauri shell setup completed");
|
||||
return std::result::Result::Ok(());
|
||||
});
|
||||
let run_result = builder.run(context);
|
||||
return match run_result {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot run KSP Raw Transaction Ingest Desk Tauri runtime")
|
||||
.with_context("tauri_error", error.to_string()),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn configure_packaged_runtime(context: &tauri::Context<tauri::Wry>) -> ksp_core_lib::Result<()> {
|
||||
if cfg!(debug_assertions) {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
let resource_root = tauri::utils::platform::resource_dir(context.package_info(), &tauri::Env::default());
|
||||
let resource_root = match resource_root {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot resolve packaged Tauri resource directory")
|
||||
.with_context("tauri_error", error.to_string()),
|
||||
);
|
||||
},
|
||||
};
|
||||
let layout = ksp_config_lib::prepare_packaged_runtime(resource_root.as_path());
|
||||
let layout = match layout {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let working_directory = std::env::set_current_dir(layout.runtime_root());
|
||||
return match working_directory {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot activate packaged KSP runtime directory")
|
||||
.with_context("runtime_root", layout.runtime_root().to_string_lossy().into_owned())
|
||||
.with_source(error),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(clippy::question_mark_used)] // Tauri generates the question-mark operator internally for async command dispatch.
|
||||
fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
return builder.invoke_handler(tauri::generate_handler![
|
||||
emit_frontend_log,
|
||||
get_route_foundation,
|
||||
get_route_inventory,
|
||||
get_route_monitoring,
|
||||
get_runtime_status,
|
||||
splash_frontend_ready,
|
||||
start_route,
|
||||
stop_route,
|
||||
validate_route_start
|
||||
]);
|
||||
}
|
||||
|
||||
fn configure_window_events(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
return builder.on_window_event(|window, event| {
|
||||
if window.label() != "main" {
|
||||
return;
|
||||
}
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||
api.prevent_close();
|
||||
let app_handle = window.app_handle().clone();
|
||||
let state = app_handle.state::<crate::AppState>();
|
||||
let begin = state.begin_shutdown();
|
||||
let begin = match begin {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
error_domain = error.code().domain(),
|
||||
error_code = error.code().code(),
|
||||
"Raw Transaction Ingest Desk could not enter graceful shutdown; exiting after safe error projection"
|
||||
);
|
||||
app_handle.exit(1);
|
||||
return;
|
||||
},
|
||||
};
|
||||
if !begin {
|
||||
return;
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
"Raw Transaction Ingest Desk main-window close requested; route Start admission closed and cooperative shutdown requested"
|
||||
);
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let state = app_handle.state::<crate::AppState>();
|
||||
let shutdown = state.shutdown_routes().await;
|
||||
match shutdown {
|
||||
std::result::Result::Ok(()) => {
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
"Raw Transaction Ingest Desk route and shared Store shutdown completed; exiting application"
|
||||
);
|
||||
app_handle.exit(0);
|
||||
},
|
||||
std::result::Result::Err(error) => {
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
error_domain = error.code().domain(),
|
||||
error_code = error.code().code(),
|
||||
"Raw Transaction Ingest Desk bounded route shutdown failed; exiting application after bounded cleanup attempt"
|
||||
);
|
||||
app_handle.exit(1);
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn project_command_error(command: &'static str, domain: &'static str, error: &ksp_core_lib::Error) -> crate::CommandErrorDto {
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = domain,
|
||||
command = command,
|
||||
error_domain = error.code().domain(),
|
||||
error_code = error.code().code(),
|
||||
"Raw Transaction Ingest Desk Tauri command failed"
|
||||
);
|
||||
return crate::CommandErrorDto::from_error(error);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
|
||||
let result = crate::emit_frontend_log_event(payload);
|
||||
return match result {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("emit_frontend_log", crate::TRACING_DOMAIN_FRONTEND, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_route_foundation(state: tauri::State<'_, crate::AppState>) -> crate::RawIngestRouteFoundationDto {
|
||||
return state.route_foundation();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_route_inventory(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::RawIngestRouteInventoryDto, crate::CommandErrorDto> {
|
||||
let result = state.route_inventory();
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("get_route_inventory", crate::TRACING_DOMAIN_INVENTORY, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_route_monitoring(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<std::vec::Vec<crate::RawIngestRouteMonitoringDto>, crate::CommandErrorDto> {
|
||||
let result = state.route_monitoring();
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("get_route_monitoring", crate::TRACING_DOMAIN_ROUTE_RUNTIME, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::ShellStatusDto, crate::CommandErrorDto> {
|
||||
let result = state.shell_status();
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("get_runtime_status", crate::TRACING_DOMAIN_SHELL, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn splash_frontend_ready(
|
||||
app: tauri::AppHandle,
|
||||
webview_window: tauri::WebviewWindow,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<(), crate::CommandErrorDto> {
|
||||
let result = crate::splash_frontend_ready_service(app, webview_window, &state).await;
|
||||
return match result {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("splash_frontend_ready", crate::TRACING_DOMAIN_WINDOWS, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn start_route(
|
||||
app: tauri::AppHandle,
|
||||
request: crate::RawIngestRouteStartRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::RawIngestRouteRuntimeDto, crate::CommandErrorDto> {
|
||||
let launch = state.start_route(&request).await;
|
||||
let launch = match launch {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(project_command_error("start_route", crate::TRACING_DOMAIN_ROUTE_RUNTIME, &error));
|
||||
},
|
||||
};
|
||||
let acknowledgement = launch.acknowledgement();
|
||||
let monitoring_identity = launch.monitoring_identity();
|
||||
let monitoring_source = launch.monitoring_source();
|
||||
let monitoring_app = app.clone();
|
||||
let monitoring_task = tauri::async_runtime::spawn(async move {
|
||||
monitor_route_status(monitoring_app, monitoring_identity, monitoring_source).await;
|
||||
});
|
||||
std::mem::drop(monitoring_task);
|
||||
let terminal_task = tauri::async_runtime::spawn(async move {
|
||||
launch.monitor().await;
|
||||
});
|
||||
std::mem::drop(terminal_task);
|
||||
return std::result::Result::Ok(acknowledgement);
|
||||
}
|
||||
|
||||
const ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS: u64 = 500;
|
||||
|
||||
async fn monitor_route_status(
|
||||
app: tauri::AppHandle,
|
||||
runtime: crate::RawIngestRouteRuntimeDto,
|
||||
source: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSnapshotSource,
|
||||
) {
|
||||
let mut snapshot = source.current();
|
||||
loop {
|
||||
let projected = crate::project_route_monitoring(&runtime, &snapshot);
|
||||
match projected {
|
||||
std::result::Result::Ok(status) => {
|
||||
let main = app.get_webview_window("main");
|
||||
if let std::option::Option::Some(window) = main {
|
||||
let emitted = window.emit(crate::RAW_INGEST_ROUTE_STATUS_EVENT_NAME, status);
|
||||
if emitted.is_err() {
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
|
||||
"Raw Transaction Ingest Desk could not emit latest-value route monitoring status"
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
std::result::Result::Err(error) => {
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
|
||||
error_domain = error.code().domain(),
|
||||
error_code = error.code().code(),
|
||||
"Raw Transaction Ingest Desk could not project latest-value route monitoring status"
|
||||
);
|
||||
},
|
||||
}
|
||||
if snapshot.worker_snapshot().state().is_terminal() {
|
||||
return;
|
||||
}
|
||||
let observed = snapshot.worker_snapshot().sequence();
|
||||
snapshot = source.wait_for_change(observed).await;
|
||||
if !snapshot.worker_snapshot().state().is_terminal() {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS)).await;
|
||||
snapshot = source.current();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn stop_route(
|
||||
request: crate::RawIngestRouteStopRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::RawIngestRouteRuntimeDto, crate::CommandErrorDto> {
|
||||
let result = state.stop_route(&request).await;
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("stop_route", crate::TRACING_DOMAIN_ROUTE_RUNTIME, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn validate_route_start(
|
||||
request: crate::RawIngestRouteStartRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::RawIngestRouteStartValidationDto, crate::CommandErrorDto> {
|
||||
let result = state.validate_route_start(&request);
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("validate_route_start", crate::TRACING_DOMAIN_ROUTE_START, &error)),
|
||||
};
|
||||
}
|
||||
53
crates/ksp-app-raw-transaction-ingest-desk/src/tw_main.rs
Normal file
53
crates/ksp-app-raw-transaction-ingest-desk/src/tw_main.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/tw_main.rs
|
||||
// version: 1
|
||||
|
||||
//! Tauri-window helpers for the Raw Transaction Ingest Desk main window.
|
||||
|
||||
use tauri::Manager; // rust-rules: trait-import
|
||||
|
||||
/// Crate-internal `WINDOW_LABEL_MAIN` constant.
|
||||
pub(crate) const WINDOW_LABEL_MAIN: &str = "main";
|
||||
|
||||
/// Resolves the required main window or returns a typed error.
|
||||
pub(crate) fn require_main_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
|
||||
let window = manager.get_webview_window(WINDOW_LABEL_MAIN);
|
||||
return match window {
|
||||
std::option::Option::Some(value) => std::result::Result::Ok(value),
|
||||
std::option::Option::None => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_MISSING, "Raw Transaction Ingest Desk main window is missing")
|
||||
.with_context("window_label", WINDOW_LABEL_MAIN),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// Shows main window.
|
||||
pub(crate) fn show_main_window(app: &tauri::AppHandle) -> ksp_core_lib::Result<()> {
|
||||
let window = crate::require_main_window(app);
|
||||
let window = match window {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let shown = window.show();
|
||||
if let std::result::Result::Err(error) = shown {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot show Raw Transaction Ingest Desk main window")
|
||||
.with_context("window_label", WINDOW_LABEL_MAIN)
|
||||
.with_source(error),
|
||||
);
|
||||
}
|
||||
let focused = window.set_focus();
|
||||
if let std::result::Result::Err(error) = focused {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot focus Raw Transaction Ingest Desk main window")
|
||||
.with_context("window_label", WINDOW_LABEL_MAIN)
|
||||
.with_source(error),
|
||||
);
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
window_label = WINDOW_LABEL_MAIN,
|
||||
"main window shown and focused"
|
||||
);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
173
crates/ksp-app-raw-transaction-ingest-desk/src/tw_splash.rs
Normal file
173
crates/ksp-app-raw-transaction-ingest-desk/src/tw_splash.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/tw_splash.rs
|
||||
// version: 1
|
||||
|
||||
//! Tauri-window lifecycle for the Raw Transaction Ingest Desk splash window.
|
||||
|
||||
use tauri::Emitter; // rust-rules: trait-import
|
||||
use tauri::Manager; // rust-rules: trait-import
|
||||
|
||||
/// Crate-internal splash window label.
|
||||
pub(crate) const WINDOW_LABEL_SPLASH: &str = "splash";
|
||||
|
||||
const SPLASH_EVENT_NAME: &str = "ksp-splash-order";
|
||||
|
||||
/// Resolves the required splash window or returns a typed error.
|
||||
pub(crate) fn require_splash_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
|
||||
let window = manager.get_webview_window(WINDOW_LABEL_SPLASH);
|
||||
return match window {
|
||||
std::option::Option::Some(value) => std::result::Result::Ok(value),
|
||||
std::option::Option::None => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_MISSING, "Raw Transaction Ingest Desk splash window is missing")
|
||||
.with_context("window_label", WINDOW_LABEL_SPLASH),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// Starts the one-shot splash lifecycle after the splash frontend reports readiness.
|
||||
pub(crate) async fn splash_frontend_ready_service(
|
||||
app: tauri::AppHandle,
|
||||
invoking_window: tauri::WebviewWindow,
|
||||
state: &crate::AppState,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
if invoking_window.label() != WINDOW_LABEL_SPLASH {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_ORIGIN_INVALID, "Splash readiness may only originate from the splash window")
|
||||
.with_context("window_label", invoking_window.label()),
|
||||
);
|
||||
}
|
||||
if !state.begin_splash_sequence() {
|
||||
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, "duplicate splash frontend readiness ignored");
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
let settings = state.splash_settings();
|
||||
let lifecycle_started = std::time::Instant::now();
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
window_label = WINDOW_LABEL_SPLASH,
|
||||
fade_in_ms = settings.fade_in_ms(),
|
||||
fade_in_source = settings.fade_in_source(),
|
||||
minimum_ms = settings.minimum_ms(),
|
||||
minimum_source = settings.minimum_source(),
|
||||
fade_out_ms = settings.fade_out_ms(),
|
||||
fade_out_source = settings.fade_out_source(),
|
||||
expected_backend_lifecycle_ms = settings.expected_backend_lifecycle_ms(),
|
||||
"splash frontend readiness accepted; starting splash to main lifecycle"
|
||||
);
|
||||
let emitted = emit_order(
|
||||
&invoking_window,
|
||||
crate::SplashOrderDto::new("fade_in", std::option::Option::None, std::option::Option::None, std::option::Option::Some(settings.fade_in_ms())),
|
||||
);
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let emitted = emit_message(&invoking_window, "Initialisation de Raw Transaction Ingest Desk...", "info");
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let emitted = emit_debug(&invoking_window, "Listener frontend prêt; fade-in piloté par Rust.");
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
sleep_and_log(&invoking_window, "fade-in", u64::from(settings.fade_in_ms()), lifecycle_started).await;
|
||||
let emitted = emit_message(&invoking_window, "Configuration KSP chargée.", "success");
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let emitted = emit_debug(&invoking_window, "Configuration et provenance des timings résolues via ksp-config-lib.");
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let emitted = emit_message(&invoking_window, "Runtime Logging initialisé.", "success");
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let emitted = emit_message(&invoking_window, "Scaffold desktop prêt.", "success");
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
sleep_and_log(&invoking_window, "minimum", settings.minimum_ms(), lifecycle_started).await;
|
||||
let emitted = emit_message(&invoking_window, "Initialisation terminée.", "success");
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let emitted = emit_order(
|
||||
&invoking_window,
|
||||
crate::SplashOrderDto::new("fade_out", std::option::Option::None, std::option::Option::None, std::option::Option::Some(settings.fade_out_ms())),
|
||||
);
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
sleep_and_log(&invoking_window, "fade-out", u64::from(settings.fade_out_ms()), lifecycle_started).await;
|
||||
let show_main = crate::show_main_window(&app);
|
||||
if let std::result::Result::Err(error) = show_main {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let destroyed = invoking_window.destroy();
|
||||
if let std::result::Result::Err(error) = destroyed {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot destroy Raw Transaction Ingest Desk splash window")
|
||||
.with_context("window_label", WINDOW_LABEL_SPLASH)
|
||||
.with_source(error),
|
||||
);
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
window_label = WINDOW_LABEL_SPLASH,
|
||||
expected_backend_lifecycle_ms = settings.expected_backend_lifecycle_ms(),
|
||||
actual_backend_lifecycle_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0,
|
||||
"splash window destroyed after main activation"
|
||||
);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn emit_message(window: &tauri::WebviewWindow, message: &str, status: &str) -> ksp_core_lib::Result<()> {
|
||||
return emit_order(
|
||||
window,
|
||||
crate::SplashOrderDto::new("add_message", std::option::Option::Some(message), std::option::Option::Some(status), std::option::Option::None),
|
||||
);
|
||||
}
|
||||
|
||||
fn emit_debug(window: &tauri::WebviewWindow, message: &str) -> ksp_core_lib::Result<()> {
|
||||
if !cfg!(debug_assertions) {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
return emit_order(
|
||||
window,
|
||||
crate::SplashOrderDto::new("add_debug", std::option::Option::Some(message), std::option::Option::None, std::option::Option::None),
|
||||
);
|
||||
}
|
||||
|
||||
async fn sleep_and_log(window: &tauri::WebviewWindow, phase: &str, duration_ms: u64, lifecycle_started: std::time::Instant) {
|
||||
let wait_started = std::time::Instant::now();
|
||||
tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await;
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
window_label = window.label(),
|
||||
phase,
|
||||
configured_wait_ms = duration_ms,
|
||||
actual_wait_ms = wait_started.elapsed().as_secs_f64() * 1000.0,
|
||||
lifecycle_elapsed_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0,
|
||||
"splash lifecycle wait completed"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
fn emit_order(window: &tauri::WebviewWindow, order: crate::SplashOrderDto) -> ksp_core_lib::Result<()> {
|
||||
let action = order.action.clone();
|
||||
let emitted = window.emit(SPLASH_EVENT_NAME, order);
|
||||
return match emitted {
|
||||
std::result::Result::Ok(()) => {
|
||||
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, action = action.as_str(), "splash order emitted");
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot emit Raw Transaction Ingest Desk splash order")
|
||||
.with_context("window_label", WINDOW_LABEL_SPLASH)
|
||||
.with_context("action", action)
|
||||
.with_source(error),
|
||||
),
|
||||
};
|
||||
}
|
||||
73
crates/ksp-app-raw-transaction-ingest-desk/tauri.conf.json
Normal file
73
crates/ksp-app-raw-transaction-ingest-desk/tauri.conf.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "KSP Raw Transaction Ingest Desk",
|
||||
"identifier": "com.sasedev.ksp-app-raw-transaction-ingest-desk",
|
||||
"build": {
|
||||
"beforeDevCommand": {
|
||||
"script": "npm run dev",
|
||||
"cwd": "."
|
||||
},
|
||||
"devUrl": "http://localhost:1440",
|
||||
"beforeBuildCommand": {
|
||||
"script": "npm run build",
|
||||
"cwd": "."
|
||||
},
|
||||
"frontendDist": "../../../builds/khadhroony-solana-project/ksp-app-raw-transaction-ingest-desk/dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"label": "splash",
|
||||
"url": "splash.html",
|
||||
"title": "Chargement — KSP Raw Transaction Ingest Desk",
|
||||
"width": 960,
|
||||
"height": 637,
|
||||
"resizable": false,
|
||||
"decorations": false,
|
||||
"transparent": true,
|
||||
"center": true,
|
||||
"alwaysOnTop": true
|
||||
},
|
||||
{
|
||||
"label": "main",
|
||||
"url": "main.html",
|
||||
"title": "KSP Raw Transaction Ingest Desk",
|
||||
"width": 1500,
|
||||
"height": 960,
|
||||
"minWidth": 1024,
|
||||
"minHeight": 768,
|
||||
"center": true,
|
||||
"visible": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/favicon.png",
|
||||
"icons/favicon.ico"
|
||||
],
|
||||
"resources": {
|
||||
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
|
||||
"../../config/composite.ksp-app-raw-transaction-ingest-desk.json": "config/composite.ksp-app-raw-transaction-ingest-desk.json",
|
||||
"../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-desk.json",
|
||||
"../../config/composite.ksp-app-store-desk.json": "config/composite.ksp-app-store-desk.json",
|
||||
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
||||
"../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json",
|
||||
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.schema.json",
|
||||
"../../config/schemas/std.offchain_transport.schema.json": "config/schemas/std.offchain_transport.schema.json",
|
||||
"../../config/schemas/std.store.schema.json": "config/schemas/std.store.schema.json",
|
||||
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json",
|
||||
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json",
|
||||
"../../config/std.logging.json": "config/std.logging.json",
|
||||
"../../config/std.offchain_transport.json": "config/std.offchain_transport.json",
|
||||
"../../config/std.store.json": "config/std.store.json",
|
||||
"../../config/std.transport.json": "config/std.transport.json",
|
||||
"../../config/std.wallet.json": "config/std.wallet.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/dependency_boundary.rs
|
||||
// version: 5
|
||||
|
||||
//! Dependency-boundary canaries for Raw Transaction Ingest Desk Start-resource reconstruction.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
fn app_root() -> std::path::PathBuf {
|
||||
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
}
|
||||
|
||||
fn read_text(path: &std::path::Path) -> String {
|
||||
let source = std::fs::read_to_string(path);
|
||||
assert!(source.is_ok(), "unable to read {}", path.display());
|
||||
return match source {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => String::new(),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_manifest_adds_store_facade_and_worker_api_without_physical_backend_edge() {
|
||||
let manifest = read_text(app_root().join("Cargo.toml").as_path());
|
||||
for required in [
|
||||
"ksp-config-lib",
|
||||
"ksp-core-lib",
|
||||
"ksp-logging-lib",
|
||||
"ksp-onchain-transport-lib",
|
||||
"ksp-store-lib",
|
||||
"ksp-worker-api",
|
||||
"ksp-worker-raw-transaction-ingest-lib",
|
||||
"tauri-plugin-tracing",
|
||||
] {
|
||||
assert!(manifest.contains(required), "missing pre.008 dependency {required}");
|
||||
}
|
||||
for forbidden in ["ksp-store-api", "ksp-store-postgres-lib", "ksp-job-backfill-lib", "reqwest", "tokio-postgres", "tonic", "yellowstone-grpc"] {
|
||||
assert!(!manifest.contains(forbidden), "pre.008 opens forbidden direct dependency {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_route_start_reconstructs_exact_worker_sources_without_network_store_or_worker_launch() {
|
||||
let source = read_text(app_root().join("src/route_start.rs").as_path());
|
||||
for required in [
|
||||
"HttpTransportPool::new",
|
||||
"YellowstoneGrpcChannel::prepare",
|
||||
"RawTransactionIngestYellowstoneSource::new",
|
||||
"RawTransactionIngestStandardLogsSource::new",
|
||||
"RawTransactionIngestStandardBlockSource::new",
|
||||
"RawTransactionIngestHeliusTransactionSource::new",
|
||||
"RawTransactionIngestHttpBlockPollingSource::new",
|
||||
] {
|
||||
assert!(source.contains(required), "missing exact resource constructor {required}");
|
||||
}
|
||||
for forbidden in [
|
||||
"YellowstoneGrpcChannel::connect",
|
||||
"SolanaStandardWsSession::connect",
|
||||
"HeliusWsSession::connect",
|
||||
"Store::open",
|
||||
"RawTransactionIngestWorker::",
|
||||
"start_with_runtime_resources",
|
||||
"request_stop",
|
||||
] {
|
||||
assert!(!source.contains(forbidden), "pre.007 performs premature runtime operation {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_route_runtime_owns_real_store_worker_start_stop_without_physical_backend_types() {
|
||||
let source = read_text(app_root().join("src/route_runtime.rs").as_path());
|
||||
for required in [
|
||||
"Store::open",
|
||||
"StoreHealthState::Ready",
|
||||
"RawTransactionIngestWorker::start_with_runtime_resources",
|
||||
"request_stop",
|
||||
"wait_terminal",
|
||||
".close().await",
|
||||
"Arc::try_unwrap",
|
||||
] {
|
||||
assert!(source.contains(required), "missing mono-route runtime operation {required}");
|
||||
}
|
||||
for forbidden in ["tokio_postgres", "ksp_store_postgres_lib", "PostgresStore", "connection_uri", "endpoint_url"] {
|
||||
assert!(!source.contains(forbidden), "pre.008 crosses physical or secret boundary {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_010_monitoring_projects_existing_worker_snapshot_facade_without_new_backend_edges() {
|
||||
let source = read_text(app_root().join("src/route_monitoring.rs").as_path());
|
||||
for required in ["RawTransactionIngestSnapshot", "worker_snapshot()", "source_reconnect_total()", "source_replay_attempt_total()", "gaps()"] {
|
||||
assert!(source.contains(required), "missing Worker snapshot facade marker {required}");
|
||||
}
|
||||
for forbidden in ["tokio_postgres", "ksp_store_postgres_lib", "reqwest", "tonic", "yellowstone_grpc_client"] {
|
||||
assert!(!source.contains(forbidden), "monitoring opens forbidden backend edge {forbidden}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_contract.rs
|
||||
// version: 13
|
||||
|
||||
//! Desktop scaffold contract canaries for Raw Transaction Ingest Desk.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
fn app_root() -> std::path::PathBuf {
|
||||
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
}
|
||||
|
||||
fn read_text(path: &std::path::Path) -> String {
|
||||
let source = std::fs::read_to_string(path);
|
||||
assert!(source.is_ok(), "unable to read {}", path.display());
|
||||
return match source {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => String::new(),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_vite_and_tauri_identity_use_reserved_ports_and_crate_local_hooks() {
|
||||
let config = std::fs::read_to_string(app_root().join("tauri.conf.json"));
|
||||
assert!(config.is_ok());
|
||||
let config = match config {
|
||||
std::result::Result::Ok(value) => serde_json::from_str::<serde_json::Value>(value.as_str()),
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert!(config.is_ok());
|
||||
if let std::result::Result::Ok(config) = config {
|
||||
assert_eq!(config["productName"], "KSP Raw Transaction Ingest Desk");
|
||||
assert_eq!(config["identifier"], "com.sasedev.ksp-app-raw-transaction-ingest-desk");
|
||||
assert_eq!(config["build"]["devUrl"], "http://localhost:1440");
|
||||
}
|
||||
let vite = read_text(app_root().join("vite.config.ts").as_path());
|
||||
assert!(vite.contains("port: 1440"));
|
||||
assert!(vite.contains("strictPort: true"));
|
||||
assert!(vite.contains("port: 1441"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_rust_package_is_mixed_lib_bin_and_workspace_member() {
|
||||
let manifest = read_text(app_root().join("Cargo.toml").as_path());
|
||||
assert!(manifest.contains("name = \"ksp-app-raw-transaction-ingest-desk\""));
|
||||
assert!(manifest.contains("name = \"ksp_app_raw_transaction_ingest_desk_lib\""));
|
||||
assert!(manifest.contains("crate-type = [\"staticlib\", \"cdylib\", \"rlib\"]"));
|
||||
let workspace = read_text(app_root().join("../../Cargo.toml").as_path());
|
||||
assert!(workspace.contains("crates/ksp-app-raw-transaction-ingest-desk"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_package_dependencies_match_current_desk_baseline() {
|
||||
let package = std::fs::read_to_string(app_root().join("package.json"));
|
||||
assert!(package.is_ok());
|
||||
let package = match package {
|
||||
std::result::Result::Ok(value) => serde_json::from_str::<serde_json::Value>(value.as_str()),
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert!(package.is_ok());
|
||||
if let std::result::Result::Ok(package) = package {
|
||||
let dependencies = &package["dependencies"];
|
||||
for dependency in
|
||||
["@fltsci/tauri-plugin-tracing", "@fortawesome/fontawesome-free", "@tauri-apps/api", "bootstrap", "resize-observer-polyfill", "simplebar"]
|
||||
{
|
||||
assert!(dependencies.get(dependency).is_some(), "missing frontend dependency {dependency}");
|
||||
}
|
||||
assert!(dependencies.get("datatables.net-bs5").is_none());
|
||||
let development = &package["devDependencies"];
|
||||
for dependency in ["@tauri-apps/cli", "@types/bootstrap", "@types/node", "sass-embedded", "typescript", "vite"] {
|
||||
assert!(development.get(dependency).is_some(), "missing frontend development dependency {dependency}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_packaging_contains_current_sixteen_config_resources() {
|
||||
let config = std::fs::read_to_string(app_root().join("tauri.conf.json"));
|
||||
assert!(config.is_ok());
|
||||
let config = match config {
|
||||
std::result::Result::Ok(value) => serde_json::from_str::<serde_json::Value>(value.as_str()),
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert!(config.is_ok());
|
||||
if let std::result::Result::Ok(config) = config {
|
||||
let resources = config["bundle"]["resources"].as_object();
|
||||
assert!(resources.is_some());
|
||||
if let std::option::Option::Some(resources) = resources {
|
||||
assert_eq!(resources.len(), 16);
|
||||
assert!(resources.contains_key("../../config/composite.ksp-app-raw-transaction-ingest-desk.json"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_splash_assets_match_existing_ksp_desk_template_byte_exact() {
|
||||
let reference = app_root().join("../ksp-app-backfill-desk");
|
||||
for relative in ["icons/favicon.ico", "icons/favicon.png", "frontend/fonts/DOS_Amazigh.ttf", "frontend/imgs/logo.png", "frontend/imgs/splash.png"] {
|
||||
let current = std::fs::read(app_root().join(relative));
|
||||
let expected = std::fs::read(reference.join(relative));
|
||||
assert!(current.is_ok());
|
||||
assert!(expected.is_ok());
|
||||
if let (std::result::Result::Ok(current), std::result::Result::Ok(expected)) = (current, expected) {
|
||||
assert_eq!(current, expected, "binary template asset differs: {relative}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_route_inventory_is_backend_owned_and_keeps_exact_five_ids_and_seven_states() {
|
||||
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
|
||||
for route in ["YellowstoneHydrated", "StandardLogsHydrated", "StandardBlockDirect", "HeliusTransactionHydrated", "HttpBlockPolling"] {
|
||||
assert!(dto.contains(route));
|
||||
}
|
||||
for state in ["Unavailable", "Configured", "Starting", "Running", "Stopping", "Stopped", "Faulted"] {
|
||||
assert!(dto.contains(state));
|
||||
}
|
||||
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
|
||||
assert!(tauri.contains("get_route_foundation"));
|
||||
assert!(tauri.contains("get_route_inventory"));
|
||||
for forbidden in ["start_worker", "stop_worker", "RawTransactionIngestWorker", "Store::open"] {
|
||||
assert!(!tauri.contains(forbidden), "scaffold Tauri surface contains premature runtime marker {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_fix_002_shell_reuses_common_light_template_and_bounds_route_foundation() {
|
||||
let html = read_text(app_root().join("frontend/main.html").as_path());
|
||||
for required in [
|
||||
"<header class=\"app-header\">",
|
||||
"navbar h-100 py-0 bg-light text-dark",
|
||||
"col-auto app-sidebar border-end bg-body-tertiary",
|
||||
"col app-content app-scrollable h-100",
|
||||
"card app-shell-card mx-auto shadow-sm border-0",
|
||||
"id=\"routeFoundation\" class=\"row g-3 app-route-grid\"",
|
||||
] {
|
||||
assert!(html.contains(required), "missing common shell marker: {required}");
|
||||
}
|
||||
for forbidden in ["app-header bg-dark", "app-sidebar bg-dark text-light", "app-content flex-grow-1"] {
|
||||
assert!(!html.contains(forbidden), "obsolete shell marker remains: {forbidden}");
|
||||
}
|
||||
let styles = read_text(app_root().join("frontend/sass/_app.scss").as_path());
|
||||
for required in [
|
||||
".app-scrollable {",
|
||||
".app-shell-card {",
|
||||
".app-route-grid {",
|
||||
".app-route-grid > * {",
|
||||
"min-width: 0;",
|
||||
"max-width: 100%;",
|
||||
"overflow: hidden;",
|
||||
"overflow-wrap: anywhere;",
|
||||
] {
|
||||
assert!(styles.contains(required), "missing bounded layout marker: {required}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_frontend_projects_profile_network_generation_and_safe_route_reasons() {
|
||||
let html = read_text(app_root().join("frontend/main.html").as_path());
|
||||
for required in ["id=\"routeProfile\"", "id=\"routeNetwork\"", "id=\"routeInventoryGeneration\"", "id=\"routeFoundation\""] {
|
||||
assert!(html.contains(required), "missing route inventory UI marker: {required}");
|
||||
}
|
||||
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
|
||||
for required in ["get_route_inventory", "missing_ws_block_capability", "missing_required_secret", "Config route inventory"] {
|
||||
assert!(main.contains(required), "missing route inventory frontend marker: {required}");
|
||||
}
|
||||
for forbidden in ["endpointUrl", "connectionUri", "apiKey", "sourceKey", "x-token"] {
|
||||
assert!(!main.contains(forbidden), "frontend exposes forbidden physical marker: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_start_preflight_is_backend_owned_generation_bound_and_does_not_launch_worker() {
|
||||
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
|
||||
for required in
|
||||
["RawIngestCommitment", "RawIngestRouteStartRequestDto", "inventory_generation", "profile_id", "route_id", "RawIngestRouteStartValidationDto"]
|
||||
{
|
||||
assert!(dto.contains(required), "missing pre.007 Start DTO marker: {required}");
|
||||
}
|
||||
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
|
||||
assert!(tauri.contains("validate_route_start"));
|
||||
for forbidden in ["start_worker", "stop_worker", "start_with_runtime_resources", "Store::open"] {
|
||||
assert!(!tauri.contains(forbidden), "pre.007 Tauri surface contains premature runtime marker {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_start_stop_controls_are_multi_route_targeted_and_backend_owned() {
|
||||
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
|
||||
for required in ["start_route", "stop_route", "RawIngestRouteStopRequestDto", "launch.monitor().await"] {
|
||||
assert!(tauri.contains(required), "missing pre.009 Tauri runtime marker {required}");
|
||||
}
|
||||
let html = read_text(app_root().join("frontend/main.html").as_path());
|
||||
for required in ["id=\"routeCommitment\"", "id=\"routeRuntimeState\"", "id=\"routeRuntimeIdentity\"", "Runtime multi-route / Store partagé"] {
|
||||
assert!(html.contains(required), "missing pre.009 frontend runtime marker {required}");
|
||||
}
|
||||
assert!(!html.contains("id=\"stopRoute\""));
|
||||
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
|
||||
for required in ["activeRuntimes", "targeted route Stop requested", "start_route", "stop_route", "RawIngestRouteStopRequestDto"] {
|
||||
assert!(main.contains(required), "missing pre.009 frontend control marker {required}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_010_runtime_monitoring_is_backend_owned_event_driven_and_resynchronizable() {
|
||||
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
|
||||
for required in [
|
||||
"get_route_monitoring",
|
||||
"monitor_route_status",
|
||||
"window.emit(crate::RAW_INGEST_ROUTE_STATUS_EVENT_NAME, status)",
|
||||
"source.wait_for_change(observed).await",
|
||||
] {
|
||||
assert!(tauri.contains(required), "missing pre.010 monitoring bridge marker {required}");
|
||||
}
|
||||
let state = read_text(app_root().join("src/app_state.rs").as_path());
|
||||
assert!(state.contains("route_monitoring(&self)"));
|
||||
assert!(state.contains("pre.010-runtime-monitoring"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_011_frontend_consumes_latest_value_monitoring_and_renders_full_route_supervision() {
|
||||
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
|
||||
for required in [
|
||||
"listen<RawIngestRouteMonitoringDto>",
|
||||
"ksp-raw-ingest-route-status",
|
||||
"get_route_monitoring",
|
||||
"routeMonitoring",
|
||||
"syncRouteMonitoring",
|
||||
"persistedTotal",
|
||||
"sourceActive",
|
||||
"openGapCount",
|
||||
"repairTransactionHydrationTotal",
|
||||
"route supervision selected",
|
||||
"route monitoring resynchronization requested",
|
||||
] {
|
||||
assert!(main.contains(required), "missing pre.011 frontend monitoring marker {required}");
|
||||
}
|
||||
let html = read_text(app_root().join("frontend/main.html").as_path());
|
||||
for required in [
|
||||
"id=\"refreshRouteMonitoring\"",
|
||||
"id=\"routeMonitoringDetail\"",
|
||||
"id=\"monitorPersistedTotal\"",
|
||||
"id=\"monitorSourceCounts\"",
|
||||
"id=\"monitorContinuityFrontier\"",
|
||||
"id=\"monitorGapList\"",
|
||||
"id=\"headerStore\"",
|
||||
] {
|
||||
assert!(html.contains(required), "missing pre.011 supervision UI marker {required}");
|
||||
}
|
||||
let styles = read_text(app_root().join("frontend/sass/_app.scss").as_path());
|
||||
for required in [".app-route-monitoring-summary {", ".app-monitoring-detail {", ".app-monitoring-list dd", ".app-monitoring-gaps"] {
|
||||
assert!(styles.contains(required), "missing pre.011 bounded supervision style {required}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_011_fix_001_monitoring_bridge_is_bounded_and_steady_updates_preserve_route_controls() {
|
||||
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
|
||||
for required in [
|
||||
"ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS: u64 = 500",
|
||||
"tokio::time::sleep(std::time::Duration::from_millis(ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS)).await",
|
||||
"snapshot = source.current()",
|
||||
] {
|
||||
assert!(tauri.contains(required), "missing pre.011-fix.001 bounded relay marker {required}");
|
||||
}
|
||||
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
|
||||
for required in [
|
||||
"const routeCards = new Map<string, HTMLElement>()",
|
||||
"renderRouteMonitoringCard",
|
||||
"const lifecycleChanged = previous === null || previous.state !== status.state",
|
||||
"if (lifecycleChanged || !renderRouteMonitoringCard(status))",
|
||||
] {
|
||||
assert!(main.contains(required), "missing pre.011-fix.001 stable-control marker {required}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_012_window_shutdown_and_start_stop_races_are_bounded_backend_owned() {
|
||||
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
|
||||
for required in [
|
||||
"configure_window_events(builder)",
|
||||
"tauri::WindowEvent::CloseRequested",
|
||||
"api.prevent_close()",
|
||||
"state.begin_shutdown()",
|
||||
"state.shutdown_routes().await",
|
||||
"app_handle.exit(0)",
|
||||
"app_handle.exit(1)",
|
||||
] {
|
||||
assert!(tauri.contains(required), "missing pre.012 bounded shutdown marker {required}");
|
||||
}
|
||||
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
|
||||
for required in [
|
||||
"shutting_down: bool",
|
||||
"stop_requested: bool",
|
||||
"ROUTE_SHUTDOWN_TIMEOUT",
|
||||
"ensure_start_admission_open",
|
||||
"shutdown_and_wait",
|
||||
"RouteStopTarget::Starting",
|
||||
"*stop_requested = true",
|
||||
"handle.request_stop()",
|
||||
] {
|
||||
assert!(runtime.contains(required), "missing pre.012 route-race marker {required}");
|
||||
}
|
||||
let app_state = read_text(app_root().join("src/app_state.rs").as_path());
|
||||
assert!(app_state.contains("let prepared = {"));
|
||||
assert!(app_state.contains("let generation = self.inventory_generation.lock()"));
|
||||
assert!(!app_state.contains("std::mem::drop(generation)"));
|
||||
assert!(app_state.contains("self.route_runtime.ensure_start_admission_open()"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_012_fix_002_terminal_fault_logging_keeps_safe_route_identity_and_worker_cause_counters() {
|
||||
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
|
||||
let start = runtime.find("pub(crate) async fn monitor(self)");
|
||||
assert!(start.is_some(), "missing RouteRuntimeLaunch::monitor");
|
||||
let std::option::Option::Some(start) = start else {
|
||||
return;
|
||||
};
|
||||
let remainder = &runtime[start..];
|
||||
let end = remainder.find("/// Starts one exact route Worker");
|
||||
assert!(end.is_some(), "missing route runtime monitor end marker");
|
||||
let std::option::Option::Some(end) = end else {
|
||||
return;
|
||||
};
|
||||
let monitor = &remainder[..end];
|
||||
for required in [
|
||||
"worker_health = worker.health().code()",
|
||||
"fault_domain = snapshot_fault_domain",
|
||||
"fault_code = snapshot_fault_code",
|
||||
"source_failure_total = snapshot.source_failure_total()",
|
||||
"store_failure_total = snapshot.store_failure_total()",
|
||||
"content_conflict_total = snapshot.content_conflict_total()",
|
||||
"open_gap_count = snapshot.open_gap_count()",
|
||||
"future_target_coverage = snapshot.future_target_coverage()",
|
||||
"failed_source_losses_reconciled = snapshot.failed_source_losses_reconciled()",
|
||||
] {
|
||||
assert!(monitor.contains(required), "missing pre.012-fix.002 terminal diagnostic marker {required}");
|
||||
}
|
||||
for forbidden in ["endpoint_url", "connection_uri", "api_key", "source_key", "worker_id", "signature", "canonical_bytes", "wire_bytes"] {
|
||||
assert!(!monitor.contains(forbidden), "terminal diagnostic surface exposes forbidden marker {forbidden}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_security.rs
|
||||
// version: 9
|
||||
|
||||
//! Desktop security canaries for the Raw Transaction Ingest Desk scaffold.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
fn app_root() -> std::path::PathBuf {
|
||||
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
}
|
||||
|
||||
fn read_text(path: &std::path::Path) -> String {
|
||||
let source = std::fs::read_to_string(path);
|
||||
assert!(source.is_ok(), "unable to read {}", path.display());
|
||||
return match source {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => String::new(),
|
||||
};
|
||||
}
|
||||
|
||||
fn collect_typescript(root: &std::path::Path, files: &mut std::vec::Vec<std::path::PathBuf>) {
|
||||
let entries = std::fs::read_dir(root);
|
||||
if let std::result::Result::Ok(entries) = entries {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
collect_typescript(path.as_path(), files);
|
||||
} else if path.extension().and_then(std::ffi::OsStr::to_str) == std::option::Option::Some("ts") {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_capability_surface_is_core_plus_tracing_only() {
|
||||
let capability = read_text(app_root().join("capabilities/default.json").as_path());
|
||||
assert!(capability.contains("\"core:default\""));
|
||||
assert!(capability.contains("\"tracing:default\""));
|
||||
for forbidden in ["dialog:", "fs:", "http:", "shell:"] {
|
||||
assert!(!capability.contains(forbidden));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_frontend_has_no_network_persistence_or_native_dialog_surface() {
|
||||
let mut files = std::vec::Vec::new();
|
||||
collect_typescript(app_root().join("frontend/ts").as_path(), &mut files);
|
||||
assert!(!files.is_empty());
|
||||
for path in files {
|
||||
let source = read_text(path.as_path());
|
||||
for forbidden in ["fetch(", "XMLHttpRequest", "localStorage", "sessionStorage", "window.alert(", "window.confirm(", "window.prompt("] {
|
||||
assert!(!source.contains(forbidden), "{} contains forbidden frontend surface {forbidden}", path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_frontend_tracing_is_control_inventory_and_command_oriented_only() {
|
||||
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
|
||||
assert!(main.contains("frontend control clicked"));
|
||||
assert!(main.contains("navigation tab clicked"));
|
||||
assert!(main.contains("runtime status refresh requested"));
|
||||
assert!(main.contains("Config route inventory refresh requested"));
|
||||
assert!(main.contains("logical network selected"));
|
||||
let invoke = read_text(app_root().join("frontend/ts/invoke.ts").as_path());
|
||||
assert!(invoke.contains("Frontend IPC command requested"));
|
||||
for forbidden in ["apiKey", "authorization", "connectionUri", "endpointUrl", "sourceKey", "rawTransaction", "transactionPayload", "x-token"] {
|
||||
assert!(!main.contains(forbidden), "frontend contains forbidden sensitive marker {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_route_inventory_dto_surface_excludes_endpoint_secret_and_physical_store_fields() {
|
||||
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
|
||||
for required in ["RawIngestRouteInventoryDto", "RawIngestProfileInventoryDto", "RawIngestRouteUnavailableReason"] {
|
||||
assert!(dto.contains(required));
|
||||
}
|
||||
for forbidden in ["endpoint_name", "endpoint_url", "api_key", "connection_uri", "source_key", "secret_metadata", "worker_handle"] {
|
||||
assert!(!dto.contains(forbidden), "route DTO contains forbidden field marker {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_start_request_and_acknowledgement_are_logical_only_and_resource_free() {
|
||||
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
|
||||
for required in ["profile_id", "route_id", "inventory_generation", "commitment", "network"] {
|
||||
assert!(dto.contains(required), "missing safe Start field marker {required}");
|
||||
}
|
||||
for forbidden in [
|
||||
"endpoint_name",
|
||||
"endpoint_url",
|
||||
"api_key",
|
||||
"connection_uri",
|
||||
"source_key",
|
||||
"secret_metadata",
|
||||
"worker_handle",
|
||||
"http_pool",
|
||||
"ws_endpoint",
|
||||
"yellowstone_channel",
|
||||
] {
|
||||
assert!(!dto.contains(forbidden), "Start DTO exposes forbidden physical marker {forbidden}");
|
||||
}
|
||||
let route_start = read_text(app_root().join("src/route_start.rs").as_path());
|
||||
assert!(route_start.contains("ERROR_CODE_ROUTE_START_STALE_INVENTORY"));
|
||||
assert!(route_start.contains("build_route_inventory_with_environment"));
|
||||
assert!(route_start.contains("resolve_store_config_profile"));
|
||||
assert!(!route_start.contains("with_context(\"endpoint\""));
|
||||
assert!(!route_start.contains("with_context(\"provider\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_runtime_acknowledgement_and_frontend_tracing_remain_secret_and_handle_free() {
|
||||
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
|
||||
assert!(dto.contains("RawIngestRouteRuntimeDto"));
|
||||
for forbidden in ["worker_id", "worker_handle", "endpoint_url", "connection_uri", "api_key", "source_key"] {
|
||||
assert!(!dto.contains(forbidden), "runtime DTO exposes forbidden marker {forbidden}");
|
||||
}
|
||||
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
|
||||
for required in ["profileId", "routeId", "commitment", "state"] {
|
||||
assert!(main.contains(required), "missing safe runtime trace marker {required}");
|
||||
}
|
||||
for forbidden in ["endpointUrl", "connectionUri", "workerId", "sourceKey", "x-token", "rawTransaction"] {
|
||||
assert!(!main.contains(forbidden), "frontend runtime trace exposes forbidden marker {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_010_monitoring_projection_is_source_neutral_payload_free_and_javascript_exact() {
|
||||
let monitoring = read_text(app_root().join("src/route_monitoring.rs").as_path());
|
||||
for required in [
|
||||
"sequence: String",
|
||||
"processing_frontier_slot: std::option::Option<String>",
|
||||
"continuity_frontier_slot: std::option::Option<String>",
|
||||
"fault_domain",
|
||||
"fault_code",
|
||||
"gaps: std::vec::Vec<crate::RawIngestRouteGapDto>",
|
||||
] {
|
||||
assert!(monitoring.contains(required), "missing pre.010 safe monitoring marker {required}");
|
||||
}
|
||||
for forbidden in ["endpoint_url", "connection_uri", "api_key", "source_key", "worker_id", "x-token"] {
|
||||
assert!(!monitoring.contains(forbidden), "monitoring projection exposes forbidden physical marker {forbidden}");
|
||||
}
|
||||
let dto_start = monitoring.find("pub(crate) struct RawIngestRouteMonitoringDto");
|
||||
assert!(dto_start.is_some(), "monitoring DTO declaration is missing");
|
||||
let dto_start = match dto_start {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let dto_end = monitoring[dto_start..].find("/// Projects one complete concrete Worker snapshot");
|
||||
assert!(dto_end.is_some(), "monitoring DTO declaration boundary is missing");
|
||||
let dto_end = match dto_end {
|
||||
std::option::Option::Some(value) => dto_start + value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let dto_surface = &monitoring[dto_start..dto_end];
|
||||
for forbidden in ["raw_transaction", "signature", "payload", "canonical_bytes", "wire_bytes", "transaction_bytes"] {
|
||||
assert!(!dto_surface.contains(forbidden), "monitoring DTO exposes forbidden RAW marker {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_011_frontend_monitoring_tracing_is_source_neutral_and_does_not_log_raw_material() {
|
||||
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
|
||||
for required in [
|
||||
"route supervision selected",
|
||||
"route monitoring latest value applied",
|
||||
"route monitoring resynchronization requested",
|
||||
"route monitoring refresh button clicked",
|
||||
"commitment selection changed",
|
||||
"routeId",
|
||||
"sequence",
|
||||
"statusCount",
|
||||
] {
|
||||
assert!(main.contains(required), "missing pre.011 safe tracing marker {required}");
|
||||
}
|
||||
for forbidden in [
|
||||
"endpointUrl",
|
||||
"connectionUri",
|
||||
"apiKey",
|
||||
"sourceKey",
|
||||
"workerId",
|
||||
"x-token",
|
||||
"rawTransaction",
|
||||
"transactionPayload",
|
||||
"canonicalBytes",
|
||||
"wireBytes",
|
||||
"transactionBytes",
|
||||
"signature",
|
||||
] {
|
||||
assert!(!main.contains(forbidden), "pre.011 frontend exposes forbidden marker {forbidden}");
|
||||
}
|
||||
assert!(!main.contains("frontendTrace(\"main\", \"Raw Transaction Ingest Desk route monitoring latest value applied\", status"));
|
||||
assert!(!main.contains("frontendDebug(\"main\", \"Raw Transaction Ingest Desk route monitoring resynchronization completed\", statuses"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_012_hostile_ipc_is_shape_strict_bounded_and_safely_projected() {
|
||||
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
|
||||
assert!(dto.matches("deny_unknown_fields").count() >= 2);
|
||||
assert!(dto.contains("RAW_INGEST_PROFILE_ID_MAX_BYTES: usize = 256"));
|
||||
assert!(dto.contains("validate_logical_identity"));
|
||||
assert!(dto.contains("ERROR_CODE_ROUTE_REQUEST_INVALID"));
|
||||
let frontend_logging = read_text(app_root().join("src/frontend_logging.rs").as_path());
|
||||
for required in [
|
||||
"deny_unknown_fields",
|
||||
"FRONTEND_LOG_MESSAGE_MAX_BYTES: usize = 8192",
|
||||
"FRONTEND_LOG_SELECTOR_MAX_BYTES: usize = 16",
|
||||
"payload.message.chars().any(char::is_control)",
|
||||
"ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID",
|
||||
"eq_ignore_ascii_case",
|
||||
] {
|
||||
assert!(frontend_logging.contains(required), "missing pre.012 frontend IPC guard {required}");
|
||||
}
|
||||
assert!(!frontend_logging.contains("to_ascii_lowercase()"));
|
||||
let frontend = read_text(app_root().join("frontend/ts/frontend_log.ts").as_path());
|
||||
assert!(frontend.contains("FRONTEND_LOG_MESSAGE_MAX_CODE_UNITS = 1024"));
|
||||
assert!(frontend.contains("boundMessage(message)"));
|
||||
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
|
||||
assert!(tauri.contains("error_domain = error.code().domain()"));
|
||||
assert!(tauri.contains("error_code = error.code().code()"));
|
||||
assert!(!tauri.contains("error = ?error"));
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/public_api.rs
|
||||
// version: 1
|
||||
|
||||
//! Public API checks for the KSP Raw Transaction Ingest desktop application library.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
#[test]
|
||||
fn run_entry_point_is_available_from_crate_root() {
|
||||
let run_entry_point: fn(&[std::ffi::OsString]) -> ksp_core_lib::Result<()> = ksp_app_raw_transaction_ingest_desk_lib::run;
|
||||
assert_eq!(std::mem::size_of_val(&run_entry_point), std::mem::size_of::<fn(&[std::ffi::OsString]) -> ksp_core_lib::Result<()>>());
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/release_completeness.rs
|
||||
// version: 12
|
||||
|
||||
//! Release-completeness canaries for Raw Transaction Ingest Desk Start-resource reconstruction.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
fn app_root() -> std::path::PathBuf {
|
||||
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
}
|
||||
|
||||
fn read_text(path: &std::path::Path) -> String {
|
||||
let source = std::fs::read_to_string(path);
|
||||
assert!(source.is_ok(), "unable to read {}", path.display());
|
||||
return match source {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => String::new(),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_010_production_module_inventory_adds_only_route_monitoring_to_pre_009_surface() {
|
||||
let lib = read_text(app_root().join("src/lib.rs").as_path());
|
||||
let expected = [
|
||||
"mod app_state;",
|
||||
"mod bootstrap;",
|
||||
"mod constants;",
|
||||
"mod dto_common;",
|
||||
"mod dto_route;",
|
||||
"mod errors;",
|
||||
"mod frontend_logging;",
|
||||
"mod logging_runtime;",
|
||||
"mod route_inventory;",
|
||||
"mod route_monitoring;",
|
||||
"mod route_runtime;",
|
||||
"mod route_start;",
|
||||
"mod splash;",
|
||||
"mod tauri;",
|
||||
"mod tw_main;",
|
||||
"mod tw_splash;",
|
||||
];
|
||||
assert_eq!(lib.matches("mod ").count(), expected.len());
|
||||
for module in expected {
|
||||
assert!(lib.contains(module));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_public_surface_still_exposes_only_application_run_entry_point() {
|
||||
let lib = read_text(app_root().join("src/lib.rs").as_path());
|
||||
let public_reexports = lib.lines().filter(|line| return line.starts_with("pub use ")).collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(public_reexports, vec!["pub use self::tauri::run;"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_keeps_five_exact_source_reconstructions_separate_from_store_worker_lifecycle() {
|
||||
let route_start = read_text(app_root().join("src/route_start.rs").as_path());
|
||||
for required in [
|
||||
"RawTransactionIngestYellowstoneSource::new",
|
||||
"RawTransactionIngestStandardLogsSource::new",
|
||||
"RawTransactionIngestStandardBlockSource::new",
|
||||
"RawTransactionIngestHeliusTransactionSource::new",
|
||||
"RawTransactionIngestHttpBlockPollingSource::new",
|
||||
"RawTransactionIngestRuntimeResources",
|
||||
] {
|
||||
assert!(route_start.contains(required), "missing pre.008 exact Worker resource marker {required}");
|
||||
}
|
||||
for forbidden in ["Store::open", "start_with_runtime_resources", "request_stop", "wait_terminal"] {
|
||||
assert!(!route_start.contains(forbidden), "route preparation owns runtime lifecycle marker {forbidden}");
|
||||
}
|
||||
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
|
||||
for required in ["Store::open", "start_with_runtime_resources", "request_stop", "wait_terminal", "StoreHealthState::Ready", "close_store_arc"] {
|
||||
assert!(runtime.contains(required), "missing pre.008 lifecycle marker {required}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_start_revalidation_reuses_inventory_and_requires_profile_network_identity() {
|
||||
let route_start = read_text(app_root().join("src/route_start.rs").as_path());
|
||||
for required in [
|
||||
"current_generation == 0",
|
||||
"request.inventory_generation != current_generation",
|
||||
"build_route_inventory_with_environment",
|
||||
"request.profile_id",
|
||||
"resolve_store_config_profile",
|
||||
"store.settings().network().as_str() != network",
|
||||
"transport_matches_network",
|
||||
] {
|
||||
assert!(route_start.contains(required), "missing pre.007 revalidation marker {required}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_multi_route_runtime_keeps_independent_workers_on_one_same_network_store() {
|
||||
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
|
||||
for required in [
|
||||
"routes: std::vec::Vec<RouteRuntimeSlot>",
|
||||
"store: std::option::Option<std::sync::Arc<ksp_store_lib::Store>>",
|
||||
"ERROR_CODE_ROUTE_RUNTIME_NETWORK_MISMATCH",
|
||||
"already owns one Worker for the selected logical route",
|
||||
"shared_store(prepared.network.as_str())",
|
||||
"self.runtime_state.finish(self.token, terminal, monitoring)",
|
||||
"Arc::try_unwrap",
|
||||
] {
|
||||
assert!(runtime.contains(required), "missing pre.009 shared-Store marker {required}");
|
||||
}
|
||||
assert!(!runtime.contains("RouteRuntimeSlot::Idle"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_targeted_stop_is_route_scoped_and_late_terminal_safe() {
|
||||
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
|
||||
assert!(dto.contains("RawIngestRouteStopRequestDto"));
|
||||
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
|
||||
assert!(runtime.contains("stop_and_wait(&self, request: &crate::RawIngestRouteStopRequestDto)"));
|
||||
assert!(runtime.contains("terminal.profile_id == request.profile_id && terminal.route_id == request.route_id"));
|
||||
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
|
||||
assert!(tauri.contains("request: crate::RawIngestRouteStopRequestDto"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_yellowstone_uses_block_subscription_and_one_get_block_path_instead_of_per_transaction_hydration() {
|
||||
let route_start = read_text(app_root().join("src/route_start.rs").as_path());
|
||||
let start = route_start.find("fn prepare_yellowstone(");
|
||||
let end = route_start.find("fn prepare_http_polling(");
|
||||
assert!(start.is_some());
|
||||
assert!(end.is_some());
|
||||
let yellowstone = match (start, end) {
|
||||
(std::option::Option::Some(start), std::option::Option::Some(end)) if start < end => &route_start[start..end],
|
||||
_ => "",
|
||||
};
|
||||
assert!(yellowstone.contains("insert_block_filter"));
|
||||
assert!(yellowstone.contains("set_include_transactions(std::option::Option::Some(false))"));
|
||||
assert!(yellowstone.contains("single_http_role(transport.http_settings(), network, &[\"getBlock\"])"));
|
||||
assert!(!yellowstone.contains("insert_transaction_filter"));
|
||||
let workspace = read_text(app_root().join("../ksp-worker-raw-transaction-ingest-lib/src/runtime_resources.rs").as_path());
|
||||
assert!(workspace.contains("RawTransactionIngestYellowstoneMode::BlockHydration"));
|
||||
assert!(workspace.contains("fetch_yellowstone_block_ingresses"));
|
||||
assert!(workspace.contains("get_block_observed"));
|
||||
assert!(workspace.contains("RAW_TRANSACTION_INGEST_YELLOWSTONE_BLOCK_RECONCILIATION_ATTEMPTS"));
|
||||
assert!(workspace.contains("stop_receiver.changed()"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_010_latest_value_monitoring_reuses_worker_snapshot_source_and_tauri_event_bridge() {
|
||||
let monitoring = read_text(app_root().join("src/route_monitoring.rs").as_path());
|
||||
for required in [
|
||||
"RawIngestRouteMonitoringDto",
|
||||
"RawIngestRouteGapDto",
|
||||
"project_route_monitoring",
|
||||
"continuity_frontier_slot",
|
||||
"future_target_coverage",
|
||||
"source_reconnect_total",
|
||||
"source_replay_attempt_total",
|
||||
"repaired_gap_total",
|
||||
"backpressure_wait_total",
|
||||
] {
|
||||
assert!(monitoring.contains(required), "missing pre.010 monitoring marker {required}");
|
||||
}
|
||||
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
|
||||
for required in ["get_route_monitoring", "RAW_INGEST_ROUTE_STATUS_EVENT_NAME", "monitor_route_status", "source.wait_for_change(observed).await"] {
|
||||
assert!(tauri.contains(required), "missing pre.010 event/resync marker {required}");
|
||||
}
|
||||
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
|
||||
assert!(runtime.contains("monitoring_statuses"));
|
||||
assert!(runtime.contains("last_monitoring"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_011_frontend_supervision_closes_planned_ui_without_backend_module_growth() {
|
||||
let lib = read_text(app_root().join("src/lib.rs").as_path());
|
||||
assert_eq!(lib.matches("mod ").count(), 16);
|
||||
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
|
||||
for required in [
|
||||
"RawIngestRouteMonitoringDto",
|
||||
"bindRouteMonitoring",
|
||||
"renderMonitoringDetail",
|
||||
"syncRouteMonitoring(\"startup\")",
|
||||
"syncRouteMonitoring(\"start\")",
|
||||
"syncRouteMonitoring(\"stop\")",
|
||||
] {
|
||||
assert!(main.contains(required), "missing pre.011 frontend completeness marker {required}");
|
||||
}
|
||||
let html = read_text(app_root().join("frontend/main.html").as_path());
|
||||
for required in ["Supervision route", "Pipeline / persistence", "Sources / continuité / repair", "Gaps courants / récents"] {
|
||||
assert!(html.contains(required), "missing pre.011 operator supervision surface {required}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_011_fix_001_preserves_latest_value_contract_without_backend_module_growth() {
|
||||
let lib = read_text(app_root().join("src/lib.rs").as_path());
|
||||
assert_eq!(lib.matches("mod ").count(), 16);
|
||||
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
|
||||
assert!(tauri.contains("source.wait_for_change(observed).await"));
|
||||
assert!(tauri.contains("ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS: u64 = 500"));
|
||||
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
|
||||
assert!(main.contains("renderRouteMonitoringCard"));
|
||||
assert!(main.contains("renderSelectedProfile"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_012_race_shutdown_and_ipc_hardening_adds_no_production_module_or_lower_layer_growth() {
|
||||
let lib = read_text(app_root().join("src/lib.rs").as_path());
|
||||
assert_eq!(lib.matches("mod ").count(), 16);
|
||||
for required in [
|
||||
"ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID",
|
||||
"ERROR_CODE_ROUTE_REQUEST_INVALID",
|
||||
"ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN",
|
||||
"ERROR_CODE_ROUTE_RUNTIME_SHUTDOWN_FAILED",
|
||||
] {
|
||||
assert!(lib.contains(required), "missing pre.012 crate-root error export {required}");
|
||||
}
|
||||
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
|
||||
assert!(runtime.contains("stop_requested: bool"));
|
||||
assert!(runtime.contains("RouteStopTarget::Starting"));
|
||||
assert!(runtime.contains("cancelled_start_terminal"));
|
||||
assert!(runtime.contains("inner.routes.is_empty()"));
|
||||
let route_start = read_text(app_root().join("src/route_start.rs").as_path());
|
||||
assert!(route_start.contains("build_route_inventory_with_environment"));
|
||||
assert!(route_start.contains("request.validate_logical_identity()"));
|
||||
let worker_manifest = read_text(app_root().join("../ksp-worker-raw-transaction-ingest-lib/Cargo.toml").as_path());
|
||||
assert!(!worker_manifest.contains("ksp-app-raw-transaction-ingest-desk"));
|
||||
}
|
||||
31
crates/ksp-app-raw-transaction-ingest-desk/tsconfig.json
Normal file
31
crates/ksp-app-raw-transaction-ingest-desk/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"types": [
|
||||
"vite/client",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"frontend",
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/dto_common.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn command_error_projection_excludes_arbitrary_context_and_source_values() {
|
||||
let source = std::io::Error::other("secret-source");
|
||||
let error = ksp_core_lib::Error::new(ksp_core_lib::ErrorCode::new("test", "failed"), "safe message")
|
||||
.with_context("secret", "hidden")
|
||||
.with_source(source);
|
||||
let dto = crate::CommandErrorDto::from_error(&error);
|
||||
assert_eq!(dto.domain, "test");
|
||||
assert_eq!(dto.code, "failed");
|
||||
assert_eq!(dto.message, "safe message");
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/dto_route.rs
|
||||
// version: 5
|
||||
|
||||
#[test]
|
||||
fn pre_005_route_ids_are_exact_and_stably_ordered() {
|
||||
let foundation = crate::RawIngestRouteFoundationDto::scaffold();
|
||||
assert_eq!(foundation.route_ids.len(), 5);
|
||||
let value = serde_json::to_value(foundation.route_ids);
|
||||
assert!(value.is_ok());
|
||||
if let std::result::Result::Ok(value) = value {
|
||||
assert_eq!(
|
||||
value,
|
||||
serde_json::json!(["yellowstone-hydrated", "standard-logs-hydrated", "standard-block-direct", "helius-transaction-hydrated", "http-block-polling"])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_route_states_are_exact_without_runtime_claims() {
|
||||
let foundation = crate::RawIngestRouteFoundationDto::scaffold();
|
||||
assert_eq!(foundation.states.len(), 7);
|
||||
let value = serde_json::to_value(foundation.states);
|
||||
assert!(value.is_ok());
|
||||
if let std::result::Result::Ok(value) = value {
|
||||
assert_eq!(value, serde_json::json!(["unavailable", "configured", "starting", "running", "stopping", "stopped", "faulted"]));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_unavailable_reason_codes_are_exact_and_safe() {
|
||||
let values = [
|
||||
crate::RawIngestRouteUnavailableReason::ProfileUnresolved,
|
||||
crate::RawIngestRouteUnavailableReason::NetworkMismatch,
|
||||
crate::RawIngestRouteUnavailableReason::MissingHttpGetBlock,
|
||||
crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction,
|
||||
crate::RawIngestRouteUnavailableReason::MissingHttpBlockScan,
|
||||
crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability,
|
||||
crate::RawIngestRouteUnavailableReason::MissingWsBlockCapability,
|
||||
crate::RawIngestRouteUnavailableReason::MissingHeliusTransactionCapability,
|
||||
crate::RawIngestRouteUnavailableReason::MissingYellowstoneGrpc,
|
||||
crate::RawIngestRouteUnavailableReason::MissingRequiredSecret,
|
||||
];
|
||||
let serialized = serde_json::to_value(values);
|
||||
assert!(serialized.is_ok());
|
||||
if let std::result::Result::Ok(serialized) = serialized {
|
||||
assert_eq!(
|
||||
serialized,
|
||||
serde_json::json!([
|
||||
"profile_unresolved",
|
||||
"network_mismatch",
|
||||
"missing_http_get_block",
|
||||
"missing_http_get_transaction",
|
||||
"missing_http_block_scan",
|
||||
"missing_ws_logs_capability",
|
||||
"missing_ws_block_capability",
|
||||
"missing_helius_transaction_capability",
|
||||
"missing_yellowstone_grpc",
|
||||
"missing_required_secret"
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_runtime_acknowledgement_serializes_only_safe_logical_identity_and_state() {
|
||||
let dto = crate::RawIngestRouteRuntimeDto {
|
||||
commitment: crate::RawIngestCommitment::Confirmed,
|
||||
inventory_generation: 7,
|
||||
network: "mainnet".to_owned(),
|
||||
profile_id: "mainnet".to_owned(),
|
||||
route_id: crate::RawIngestRouteId::StandardBlockDirect,
|
||||
state: crate::RawIngestRouteState::Running,
|
||||
};
|
||||
let value = serde_json::to_value(dto);
|
||||
assert!(value.is_ok());
|
||||
if let std::result::Result::Ok(value) = value {
|
||||
assert_eq!(
|
||||
value,
|
||||
serde_json::json!({
|
||||
"commitment": "confirmed",
|
||||
"inventoryGeneration": 7,
|
||||
"network": "mainnet",
|
||||
"profileId": "mainnet",
|
||||
"routeId": "standard-block-direct",
|
||||
"state": "running"
|
||||
})
|
||||
);
|
||||
let encoded = value.to_string();
|
||||
for forbidden in ["endpoint", "provider", "connectionUri", "sourceKey", "workerId", "payload"] {
|
||||
assert!(!encoded.contains(forbidden));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_012_route_requests_reject_unknown_fields_and_bound_free_form_profile_identity() {
|
||||
let start = serde_json::from_value::<crate::RawIngestRouteStartRequestDto>(serde_json::json!({
|
||||
"commitment": "confirmed",
|
||||
"inventoryGeneration": 1,
|
||||
"profileId": "mainnet",
|
||||
"routeId": "http-block-polling",
|
||||
"endpointUrl": "https://forbidden.invalid"
|
||||
}));
|
||||
assert!(start.is_err());
|
||||
let stop = serde_json::from_value::<crate::RawIngestRouteStopRequestDto>(serde_json::json!({
|
||||
"profileId": "mainnet",
|
||||
"routeId": "http-block-polling",
|
||||
"workerId": "forbidden"
|
||||
}));
|
||||
assert!(stop.is_err());
|
||||
let oversized = crate::RawIngestRouteStartRequestDto {
|
||||
commitment: crate::RawIngestCommitment::Confirmed,
|
||||
inventory_generation: 1,
|
||||
profile_id: "x".repeat(super::RAW_INGEST_PROFILE_ID_MAX_BYTES + 1),
|
||||
route_id: crate::RawIngestRouteId::HttpBlockPolling,
|
||||
};
|
||||
let oversized = oversized.validate_logical_identity();
|
||||
assert!(oversized.is_err());
|
||||
if let std::result::Result::Err(error) = oversized {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_ROUTE_REQUEST_INVALID);
|
||||
}
|
||||
let control = crate::RawIngestRouteStopRequestDto { profile_id: "mainnet\n".to_owned(), route_id: crate::RawIngestRouteId::HttpBlockPolling };
|
||||
assert!(control.validate_logical_identity().is_err());
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/frontend_logging.rs
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn frontend_log_levels_are_explicit_and_case_insensitive() {
|
||||
assert!(matches!(super::parse_level(" TRACE "), std::result::Result::Ok(super::FrontendLogLevel::Trace)));
|
||||
assert!(matches!(super::parse_level("debug"), std::result::Result::Ok(super::FrontendLogLevel::Debug)));
|
||||
assert!(matches!(super::parse_level("Info"), std::result::Result::Ok(super::FrontendLogLevel::Info)));
|
||||
assert!(matches!(super::parse_level("warn"), std::result::Result::Ok(super::FrontendLogLevel::Warn)));
|
||||
assert!(matches!(super::parse_level("ERROR"), std::result::Result::Ok(super::FrontendLogLevel::Error)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_frontend_log_level_is_rejected() {
|
||||
let result = super::parse_level("notice");
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Ok(_) => return,
|
||||
std::result::Result::Err(error) => error,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frontend_log_targets_are_whitelisted() {
|
||||
assert!(matches!(super::parse_target("frontend"), std::result::Result::Ok(super::FrontendLogTarget::Frontend)));
|
||||
assert!(matches!(super::parse_target(" MAIN "), std::result::Result::Ok(super::FrontendLogTarget::Main)));
|
||||
assert!(matches!(super::parse_target("Splash"), std::result::Result::Ok(super::FrontendLogTarget::Splash)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arbitrary_frontend_log_target_is_rejected() {
|
||||
let result = super::parse_target("ksp-app-raw-transaction-ingest-desk.test.logging");
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Ok(_) => return,
|
||||
std::result::Result::Err(error) => error,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_012_frontend_log_ipc_rejects_unknown_fields_and_unbounded_or_control_messages() {
|
||||
let unknown = serde_json::from_value::<crate::FrontendLogPayloadDto>(serde_json::json!({
|
||||
"level": "info",
|
||||
"targetId": "main",
|
||||
"message": "safe",
|
||||
"endpointUrl": "https://forbidden.invalid"
|
||||
}));
|
||||
assert!(unknown.is_err());
|
||||
let oversized = crate::FrontendLogPayloadDto {
|
||||
level: "info".to_owned(),
|
||||
target_id: "main".to_owned(),
|
||||
message: "x".repeat(super::FRONTEND_LOG_MESSAGE_MAX_BYTES + 1),
|
||||
};
|
||||
let result = super::validate_payload_shape(&oversized);
|
||||
assert!(result.is_err());
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID);
|
||||
}
|
||||
let nul = crate::FrontendLogPayloadDto { level: "info".to_owned(), target_id: "main".to_owned(), message: "safe\0hostile".to_owned() };
|
||||
assert!(super::validate_payload_shape(&nul).is_err());
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_inventory.rs
|
||||
// version: 5
|
||||
|
||||
#[test]
|
||||
fn pre_006_route_requirement_projection_is_fail_closed_and_ordered() {
|
||||
let route = super::route_with_requirements(
|
||||
crate::RawIngestRouteId::StandardLogsHydrated,
|
||||
"devnet",
|
||||
&[
|
||||
(false, crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability),
|
||||
(false, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction),
|
||||
],
|
||||
);
|
||||
assert!(!route.selectable);
|
||||
assert_eq!(route.state, crate::RawIngestRouteState::Unavailable);
|
||||
assert_eq!(route.reason, std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability));
|
||||
assert_eq!(route.network.as_deref(), std::option::Option::Some("devnet"));
|
||||
let configured = super::route_with_requirements(
|
||||
crate::RawIngestRouteId::HttpBlockPolling,
|
||||
"mainnet",
|
||||
&[(true, crate::RawIngestRouteUnavailableReason::MissingHttpBlockScan)],
|
||||
);
|
||||
assert!(configured.selectable);
|
||||
assert_eq!(configured.state, crate::RawIngestRouteState::Configured);
|
||||
assert_eq!(configured.reason, std::option::Option::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_unavailable_profile_projects_all_five_routes_without_physical_material() {
|
||||
let profile =
|
||||
super::unavailable_profile("mainnet", false, std::option::Option::Some("mainnet"), crate::RawIngestRouteUnavailableReason::MissingRequiredSecret);
|
||||
assert_eq!(profile.routes.len(), 5);
|
||||
assert_eq!(profile.network.as_deref(), std::option::Option::Some("mainnet"));
|
||||
for route in profile.routes {
|
||||
assert!(!route.selectable);
|
||||
assert_eq!(route.reason, std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingRequiredSecret));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_fix_003_network_profiles_aggregate_same_network_capabilities_and_omit_undeclared_provider_routes() {
|
||||
let management = committed_management();
|
||||
assert!(management.is_ok());
|
||||
let management = match management {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let environment = ksp_config_lib::ConfigEnvironment::load();
|
||||
assert!(environment.is_ok());
|
||||
let environment = match environment {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let inventory = crate::build_route_inventory_with_environment(&management, &environment, 7);
|
||||
assert!(inventory.is_ok());
|
||||
let inventory = match inventory {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(inventory.generation, 7);
|
||||
assert_eq!(inventory.default_profile, "devnet");
|
||||
assert_eq!(
|
||||
inventory.profiles.iter().map(|profile| return profile.profile_id.as_str()).collect::<std::vec::Vec<_>>(),
|
||||
vec!["devnet", "mainnet", "testnet"]
|
||||
);
|
||||
let devnet = profile(&inventory, "devnet");
|
||||
assert!(devnet.is_some());
|
||||
if let std::option::Option::Some(devnet) = devnet {
|
||||
assert!(devnet.is_default);
|
||||
assert_eq!(devnet.network.as_deref(), std::option::Option::Some("devnet"));
|
||||
assert_optional_secret_route(devnet, crate::RawIngestRouteId::YellowstoneHydrated);
|
||||
assert_route(devnet, crate::RawIngestRouteId::StandardLogsHydrated, true, std::option::Option::None);
|
||||
assert_route(devnet, crate::RawIngestRouteId::StandardBlockDirect, true, std::option::Option::None);
|
||||
assert_optional_secret_route(devnet, crate::RawIngestRouteId::HeliusTransactionHydrated);
|
||||
assert_route(devnet, crate::RawIngestRouteId::HttpBlockPolling, true, std::option::Option::None);
|
||||
}
|
||||
let mainnet = profile(&inventory, "mainnet");
|
||||
assert!(mainnet.is_some());
|
||||
if let std::option::Option::Some(mainnet) = mainnet {
|
||||
assert_eq!(mainnet.network.as_deref(), std::option::Option::Some("mainnet"));
|
||||
assert_optional_secret_route(mainnet, crate::RawIngestRouteId::YellowstoneHydrated);
|
||||
assert_route(mainnet, crate::RawIngestRouteId::StandardLogsHydrated, true, std::option::Option::None);
|
||||
assert_route(mainnet, crate::RawIngestRouteId::StandardBlockDirect, true, std::option::Option::None);
|
||||
assert_optional_secret_route(mainnet, crate::RawIngestRouteId::HeliusTransactionHydrated);
|
||||
assert_route(mainnet, crate::RawIngestRouteId::HttpBlockPolling, true, std::option::Option::None);
|
||||
}
|
||||
let testnet = profile(&inventory, "testnet");
|
||||
assert!(testnet.is_some());
|
||||
if let std::option::Option::Some(testnet) = testnet {
|
||||
assert_eq!(testnet.network.as_deref(), std::option::Option::Some("testnet"));
|
||||
assert_optional_secret_route(testnet, crate::RawIngestRouteId::YellowstoneHydrated);
|
||||
assert_route(testnet, crate::RawIngestRouteId::StandardLogsHydrated, true, std::option::Option::None);
|
||||
assert_route(testnet, crate::RawIngestRouteId::StandardBlockDirect, true, std::option::Option::None);
|
||||
assert_route_absent(testnet, crate::RawIngestRouteId::HeliusTransactionHydrated);
|
||||
assert_eq!(testnet.routes.len(), 4);
|
||||
assert_route(testnet, crate::RawIngestRouteId::HttpBlockPolling, true, std::option::Option::None);
|
||||
}
|
||||
let serialized = serde_json::to_string(&inventory);
|
||||
assert!(serialized.is_ok());
|
||||
if let std::result::Result::Ok(serialized) = serialized {
|
||||
for forbidden in [
|
||||
"http://",
|
||||
"https://",
|
||||
"ws://",
|
||||
"wss://",
|
||||
"api-key",
|
||||
"x-token",
|
||||
"connection_uri",
|
||||
"source_key",
|
||||
"endpoint_name",
|
||||
"publicnode_mainnet",
|
||||
"helius_mainnet",
|
||||
] {
|
||||
assert!(!serialized.contains(forbidden), "safe network inventory leaked forbidden source marker {forbidden}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_fix_003_base_network_routes_remain_configured_independently_of_optional_sources() {
|
||||
let management = committed_management();
|
||||
assert!(management.is_ok());
|
||||
let management = match management {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let environment = ksp_config_lib::ConfigEnvironment::load();
|
||||
assert!(environment.is_ok());
|
||||
let environment = match environment {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let inventory = crate::build_route_inventory_with_environment(&management, &environment, 11);
|
||||
assert!(inventory.is_ok());
|
||||
let inventory = match inventory {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
for profile_id in ["devnet", "mainnet", "testnet"] {
|
||||
let selected = profile(&inventory, profile_id);
|
||||
assert!(selected.is_some());
|
||||
if let std::option::Option::Some(selected) = selected {
|
||||
assert_route(selected, crate::RawIngestRouteId::StandardLogsHydrated, true, std::option::Option::None);
|
||||
assert_route(selected, crate::RawIngestRouteId::StandardBlockDirect, true, std::option::Option::None);
|
||||
assert_route(selected, crate::RawIngestRouteId::HttpBlockPolling, true, std::option::Option::None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn committed_management() -> ksp_core_lib::Result<ksp_config_lib::ConfigManagement> {
|
||||
let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let crates_root = manifest.parent();
|
||||
let crates_root = match crates_root {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_INVENTORY_INVALID, "test manifest has no crates parent"));
|
||||
},
|
||||
};
|
||||
let workspace = crates_root.parent();
|
||||
let workspace = match workspace {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_INVENTORY_INVALID, "test crates root has no workspace parent"));
|
||||
},
|
||||
};
|
||||
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
|
||||
let bootstrap = match bootstrap {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let registry = ksp_config_lib::ConfigFileRegistry::defaults();
|
||||
let registry = match registry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry)));
|
||||
}
|
||||
|
||||
fn profile<'a>(inventory: &'a crate::RawIngestRouteInventoryDto, profile_id: &str) -> std::option::Option<&'a crate::RawIngestProfileInventoryDto> {
|
||||
return inventory.profiles.iter().find(|profile| return profile.profile_id == profile_id);
|
||||
}
|
||||
|
||||
fn assert_optional_secret_route(profile: &crate::RawIngestProfileInventoryDto, route_id: crate::RawIngestRouteId) {
|
||||
let route = profile.routes.iter().find(|route| return route.route_id == route_id);
|
||||
assert!(route.is_some());
|
||||
if let std::option::Option::Some(route) = route {
|
||||
if route.selectable {
|
||||
assert_eq!(route.state, crate::RawIngestRouteState::Configured);
|
||||
assert_eq!(route.reason, std::option::Option::None);
|
||||
} else {
|
||||
assert_eq!(route.state, crate::RawIngestRouteState::Unavailable);
|
||||
assert_eq!(route.reason, std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingRequiredSecret));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_route_absent(profile: &crate::RawIngestProfileInventoryDto, route_id: crate::RawIngestRouteId) {
|
||||
let route = profile.routes.iter().find(|route| return route.route_id == route_id);
|
||||
assert!(route.is_none());
|
||||
}
|
||||
|
||||
fn assert_route(
|
||||
profile: &crate::RawIngestProfileInventoryDto,
|
||||
route_id: crate::RawIngestRouteId,
|
||||
selectable: bool,
|
||||
reason: std::option::Option<crate::RawIngestRouteUnavailableReason>,
|
||||
) {
|
||||
let route = profile.routes.iter().find(|route| return route.route_id == route_id);
|
||||
assert!(route.is_some());
|
||||
if let std::option::Option::Some(route) = route {
|
||||
assert_eq!(route.selectable, selectable);
|
||||
assert_eq!(route.reason, reason);
|
||||
assert_eq!(route.state, if selectable { crate::RawIngestRouteState::Configured } else { crate::RawIngestRouteState::Unavailable });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_monitoring.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn pre_010_source_gap_and_repair_codes_are_exact_and_source_neutral() {
|
||||
assert_eq!(super::source_state_code(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Active), "active");
|
||||
assert_eq!(super::source_state_code(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Reconnecting), "reconnecting");
|
||||
assert_eq!(super::gap_state_code(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState::Repairing), "repairing");
|
||||
assert_eq!(super::gap_reason_code(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::SourceFailure), "source_failure");
|
||||
assert_eq!(
|
||||
super::repair_method_code(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::TransactionHydration),
|
||||
"transaction_hydration"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_010_slot_projection_uses_decimal_text_for_javascript_safe_exactness() {
|
||||
assert_eq!(super::slot_text(std::option::Option::Some(u64::MAX)), std::option::Option::Some(u64::MAX.to_string()));
|
||||
assert_eq!(super::slot_text(std::option::Option::None), std::option::Option::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_010_bounded_cardinality_projection_rejects_only_unrepresentable_values() {
|
||||
let zero = super::usize_to_u32(0, "zero");
|
||||
assert!(matches!(zero, std::result::Result::Ok(0)));
|
||||
let maximum = usize::try_from(u32::MAX);
|
||||
assert!(maximum.is_ok());
|
||||
if let std::result::Result::Ok(maximum) = maximum {
|
||||
let converted = super::usize_to_u32(maximum, "maximum");
|
||||
assert!(matches!(converted, std::result::Result::Ok(u32::MAX)));
|
||||
}
|
||||
if usize::BITS > u32::BITS {
|
||||
let overflow_value = usize::try_from(u64::from(u32::MAX) + 1);
|
||||
assert!(overflow_value.is_ok());
|
||||
if let std::result::Result::Ok(overflow_value) = overflow_value {
|
||||
let overflow = super::usize_to_u32(overflow_value, "overflow");
|
||||
assert!(overflow.is_err());
|
||||
if let std::result::Result::Err(error) = overflow {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_APP_STATE_INVALID);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_runtime.rs
|
||||
// version: 5
|
||||
|
||||
#[test]
|
||||
fn pre_009_multi_route_runtime_starts_without_workers_or_shared_store_material() {
|
||||
let state = crate::RouteRuntimeState::new();
|
||||
let inner = state.inner.lock();
|
||||
assert!(inner.is_ok());
|
||||
if let std::result::Result::Ok(inner) = inner {
|
||||
assert!(inner.routes.is_empty());
|
||||
assert!(inner.last_monitoring.is_empty());
|
||||
assert!(inner.store.is_none());
|
||||
assert!(inner.network.is_none());
|
||||
assert!(inner.store_opening_token.is_none());
|
||||
assert!(inner.store_closing_token.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_late_targeted_stop_returns_only_the_matching_terminal_route() {
|
||||
let state = crate::RouteRuntimeState::new();
|
||||
let terminal = crate::RawIngestRouteRuntimeDto {
|
||||
commitment: crate::RawIngestCommitment::Confirmed,
|
||||
inventory_generation: 9,
|
||||
network: "mainnet".to_owned(),
|
||||
profile_id: "mainnet".to_owned(),
|
||||
route_id: crate::RawIngestRouteId::YellowstoneHydrated,
|
||||
state: crate::RawIngestRouteState::Faulted,
|
||||
};
|
||||
let inner = state.inner.lock();
|
||||
assert!(inner.is_ok());
|
||||
if let std::result::Result::Ok(mut inner) = inner {
|
||||
inner.last_terminals.push(terminal.clone());
|
||||
}
|
||||
let request = crate::RawIngestRouteStopRequestDto { profile_id: "mainnet".to_owned(), route_id: crate::RawIngestRouteId::YellowstoneHydrated };
|
||||
let target = state.stop_target(&request);
|
||||
assert!(target.is_ok());
|
||||
assert!(matches!(&target, std::result::Result::Ok(super::RouteStopTarget::Terminal(_))));
|
||||
if let std::result::Result::Ok(super::RouteStopTarget::Terminal(value)) = target {
|
||||
assert_eq!(value.profile_id, terminal.profile_id);
|
||||
assert_eq!(value.route_id, terminal.route_id);
|
||||
assert_eq!(value.state, crate::RawIngestRouteState::Faulted);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_late_targeted_stop_waits_while_final_shared_store_is_closing() {
|
||||
let state = crate::RouteRuntimeState::new();
|
||||
let inner = state.inner.lock();
|
||||
assert!(inner.is_ok());
|
||||
if let std::result::Result::Ok(mut inner) = inner {
|
||||
inner.last_terminals.push(crate::RawIngestRouteRuntimeDto {
|
||||
commitment: crate::RawIngestCommitment::Finalized,
|
||||
inventory_generation: 11,
|
||||
network: "mainnet".to_owned(),
|
||||
profile_id: "mainnet".to_owned(),
|
||||
route_id: crate::RawIngestRouteId::HttpBlockPolling,
|
||||
state: crate::RawIngestRouteState::Stopped,
|
||||
});
|
||||
inner.store_closing_token = std::option::Option::Some(77);
|
||||
}
|
||||
let request = crate::RawIngestRouteStopRequestDto { profile_id: "mainnet".to_owned(), route_id: crate::RawIngestRouteId::HttpBlockPolling };
|
||||
let target = state.stop_target(&request);
|
||||
assert!(matches!(target, std::result::Result::Ok(super::RouteStopTarget::TerminalClosing)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_012_stop_racing_start_marks_the_reservation_for_cooperative_stop() {
|
||||
let state = crate::RouteRuntimeState::new();
|
||||
let identity = super::RouteRuntimeIdentity {
|
||||
commitment: crate::RawIngestCommitment::Confirmed,
|
||||
inventory_generation: 12,
|
||||
network: "mainnet".to_owned(),
|
||||
profile_id: "mainnet".to_owned(),
|
||||
route_id: crate::RawIngestRouteId::YellowstoneHydrated,
|
||||
};
|
||||
let inner = state.inner.lock();
|
||||
assert!(inner.is_ok());
|
||||
if let std::result::Result::Ok(mut inner) = inner {
|
||||
inner.network = std::option::Option::Some("mainnet".to_owned());
|
||||
inner.routes.push(super::RouteRuntimeSlot::Starting { identity: identity.clone(), stop_requested: false, token: 12 });
|
||||
}
|
||||
let request = crate::RawIngestRouteStopRequestDto { profile_id: "mainnet".to_owned(), route_id: crate::RawIngestRouteId::YellowstoneHydrated };
|
||||
let target = state.stop_target(&request);
|
||||
assert!(matches!(target, std::result::Result::Ok(super::RouteStopTarget::Starting { token: 12, .. })));
|
||||
let inner = state.inner.lock();
|
||||
assert!(inner.is_ok());
|
||||
if let std::result::Result::Ok(inner) = inner {
|
||||
assert!(matches!(inner.routes.first(), std::option::Option::Some(super::RouteRuntimeSlot::Starting { stop_requested: true, .. })));
|
||||
}
|
||||
let rollback = state.rollback(12);
|
||||
assert!(matches!(rollback, std::result::Result::Ok(std::option::Option::None)));
|
||||
let inner = state.inner.lock();
|
||||
assert!(inner.is_ok());
|
||||
if let std::result::Result::Ok(inner) = inner {
|
||||
assert!(inner.routes.is_empty());
|
||||
assert!(matches!(inner.last_terminals.first(), std::option::Option::Some(value) if value.state == crate::RawIngestRouteState::Stopped));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_012_application_shutdown_is_one_shot_and_marks_starting_routes_before_activation() {
|
||||
let state = crate::RouteRuntimeState::new();
|
||||
let identity = super::RouteRuntimeIdentity {
|
||||
commitment: crate::RawIngestCommitment::Finalized,
|
||||
inventory_generation: 13,
|
||||
network: "mainnet".to_owned(),
|
||||
profile_id: "mainnet".to_owned(),
|
||||
route_id: crate::RawIngestRouteId::HttpBlockPolling,
|
||||
};
|
||||
let inner = state.inner.lock();
|
||||
assert!(inner.is_ok());
|
||||
if let std::result::Result::Ok(mut inner) = inner {
|
||||
inner.network = std::option::Option::Some("mainnet".to_owned());
|
||||
inner.routes.push(super::RouteRuntimeSlot::Starting { identity, stop_requested: false, token: 13 });
|
||||
}
|
||||
let first = state.begin_shutdown();
|
||||
assert!(matches!(first, std::result::Result::Ok(true)));
|
||||
let second = state.begin_shutdown();
|
||||
assert!(matches!(second, std::result::Result::Ok(false)));
|
||||
let admission = state.ensure_start_admission_open();
|
||||
assert!(admission.is_err());
|
||||
if let std::result::Result::Err(error) = admission {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN);
|
||||
}
|
||||
let inner = state.inner.lock();
|
||||
assert!(inner.is_ok());
|
||||
if let std::result::Result::Ok(inner) = inner {
|
||||
assert!(inner.shutting_down);
|
||||
assert!(matches!(inner.routes.first(), std::option::Option::Some(super::RouteRuntimeSlot::Starting { stop_requested: true, .. })));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_012_fix_001_start_route_future_is_send_for_tauri_command() {
|
||||
fn assert_send_future<'a>(
|
||||
state: &'a crate::AppState,
|
||||
request: &'a crate::RawIngestRouteStartRequestDto,
|
||||
) -> impl std::future::Future<Output = ksp_core_lib::Result<crate::RouteRuntimeLaunch>> + std::marker::Send + 'a {
|
||||
return state.start_route(request);
|
||||
}
|
||||
let _assertion = assert_send_future;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_start.rs
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn pre_007_stale_generation_is_rejected_before_config_or_transport_reconstruction() {
|
||||
let management = committed_management();
|
||||
assert!(management.is_ok());
|
||||
let management = match management {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let request = crate::RawIngestRouteStartRequestDto {
|
||||
commitment: crate::RawIngestCommitment::Confirmed,
|
||||
inventory_generation: 3,
|
||||
profile_id: "devnet".to_owned(),
|
||||
route_id: crate::RawIngestRouteId::StandardBlockDirect,
|
||||
};
|
||||
let result = crate::validate_route_start(&management, 2, &request);
|
||||
assert!(result.is_err());
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_ROUTE_START_STALE_INVENTORY);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_commitments_are_bounded_to_confirmed_and_finalized() {
|
||||
assert_eq!(crate::RawIngestCommitment::Confirmed.into_transport(), ksp_onchain_transport_lib::SolanaCommitment::Confirmed);
|
||||
assert_eq!(crate::RawIngestCommitment::Finalized.into_transport(), ksp_onchain_transport_lib::SolanaCommitment::Finalized);
|
||||
assert_eq!(crate::RawIngestCommitment::Confirmed.as_str(), "confirmed");
|
||||
assert_eq!(crate::RawIngestCommitment::Finalized.as_str(), "finalized");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_public_standard_profiles_reconstruct_block_and_http_sources_without_network_io() {
|
||||
let management = committed_management();
|
||||
assert!(management.is_ok());
|
||||
let management = match management {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let inventory = crate::build_route_inventory(&management, 1);
|
||||
assert!(inventory.is_ok());
|
||||
let inventory = match inventory {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
for profile in inventory.profiles {
|
||||
for route_id in [crate::RawIngestRouteId::StandardLogsHydrated, crate::RawIngestRouteId::StandardBlockDirect, crate::RawIngestRouteId::HttpBlockPolling]
|
||||
{
|
||||
let route = profile.routes.iter().find(|route| return route.route_id == route_id);
|
||||
assert!(route.is_some(), "missing base route for {}", profile.profile_id);
|
||||
if let std::option::Option::Some(route) = route {
|
||||
assert_eq!(route.state, crate::RawIngestRouteState::Configured);
|
||||
let request = crate::RawIngestRouteStartRequestDto {
|
||||
commitment: crate::RawIngestCommitment::Confirmed,
|
||||
inventory_generation: 1,
|
||||
profile_id: profile.profile_id.clone(),
|
||||
route_id,
|
||||
};
|
||||
let prepared = crate::validate_route_start(&management, 1, &request);
|
||||
assert!(prepared.is_ok(), "failed Start-time reconstruction for {} / {}", profile.profile_id, route_id.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn committed_management() -> ksp_core_lib::Result<ksp_config_lib::ConfigManagement> {
|
||||
let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let crates_root = manifest.parent();
|
||||
let crates_root = match crates_root {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED, "test manifest has no crates parent"));
|
||||
},
|
||||
};
|
||||
let workspace = crates_root.parent();
|
||||
let workspace = match workspace {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED,
|
||||
"test crates root has no workspace parent",
|
||||
));
|
||||
},
|
||||
};
|
||||
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
|
||||
let bootstrap = match bootstrap {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let registry = ksp_config_lib::ConfigFileRegistry::defaults();
|
||||
let registry = match registry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry)));
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/splash.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn fallback_splash_settings_are_short_and_ordered() {
|
||||
let settings = crate::SplashSettings::fallback();
|
||||
assert_eq!(settings.fade_in_ms(), 300);
|
||||
assert_eq!(settings.minimum_ms(), 1200);
|
||||
assert_eq!(settings.fade_out_ms(), 300);
|
||||
assert_eq!(settings.expected_backend_lifecycle_ms(), 1800);
|
||||
assert_eq!(settings.fade_in_source(), "fallback");
|
||||
assert_eq!(settings.minimum_source(), "fallback");
|
||||
assert_eq!(settings.fade_out_source(), "fallback");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splash_order_contract_keeps_action_message_status_and_duration() {
|
||||
let order =
|
||||
crate::SplashOrderDto::new("add_message", std::option::Option::Some("ready"), std::option::Option::Some("success"), std::option::Option::Some(300));
|
||||
assert_eq!(order.action, "add_message");
|
||||
assert_eq!(order.message.as_deref(), std::option::Option::Some("ready"));
|
||||
assert_eq!(order.status.as_deref(), std::option::Option::Some("success"));
|
||||
assert_eq!(order.duration_ms, std::option::Option::Some(300));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splash_duration_parser_rejects_invalid_or_unbounded_values() {
|
||||
let malformed = super::parse_u64_setting("KSP_DESK_SPLASH_MINIMUM_MS", "invalid", 60_000);
|
||||
let oversized_minimum = super::parse_u64_setting("KSP_DESK_SPLASH_MINIMUM_MS", "60001", 60_000);
|
||||
let oversized_fade_in = super::parse_u32_setting("KSP_DESK_SPLASH_FADE_IN_MS", "10001", 10_000);
|
||||
let oversized_fade_out = super::parse_u32_setting("KSP_DESK_SPLASH_FADE_OUT_MS", "10001", 10_000);
|
||||
assert!(malformed.is_err());
|
||||
assert!(oversized_minimum.is_err());
|
||||
assert!(oversized_fade_in.is_err());
|
||||
assert!(oversized_fade_out.is_err());
|
||||
}
|
||||
72
crates/ksp-app-raw-transaction-ingest-desk/vite.config.ts
Normal file
72
crates/ksp-app-raw-transaction-ingest-desk/vite.config.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/vite.config.ts
|
||||
// version: 1
|
||||
|
||||
import { NodePackageImporter } from "sass-embedded";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolve } from "node:path";
|
||||
import { defineConfig, normalizePath } from "vite";
|
||||
|
||||
const appRoot = fileURLToPath(new URL(".", import.meta.url));
|
||||
const frontendRoot = normalizePath(resolve(appRoot, "frontend"));
|
||||
const frontendDist = normalizePath(resolve(appRoot, "../../../builds/khadhroony-solana-project/ksp-app-raw-transaction-ingest-desk/dist"));
|
||||
const devHost = process.env.TAURI_DEV_HOST;
|
||||
|
||||
export default defineConfig({
|
||||
clearScreen: false,
|
||||
root: frontendRoot,
|
||||
publicDir: false,
|
||||
input: {
|
||||
main: normalizePath(resolve(frontendRoot, "main.html")),
|
||||
splash: normalizePath(resolve(frontendRoot, "splash.html")),
|
||||
},
|
||||
build: {
|
||||
outDir: frontendDist,
|
||||
emptyOutDir: true,
|
||||
minify: true,
|
||||
sourcemap: false,
|
||||
cssCodeSplit: true,
|
||||
rolldownOptions: {
|
||||
output: {
|
||||
entryFileNames: "js/[name]-[hash].js",
|
||||
chunkFileNames: "js/chunks/[name]-[hash].js",
|
||||
assetFileNames: assetInfo => {
|
||||
const originalName = assetInfo.names[0] ?? "";
|
||||
const extension = originalName.substring(originalName.lastIndexOf(".") + 1).toLowerCase();
|
||||
if (extension === "css") {
|
||||
return "css/[name]-[hash][extname]";
|
||||
}
|
||||
if (["eot", "otf", "ttf", "woff", "woff2"].includes(extension)) {
|
||||
return "fonts/[name]-[hash][extname]";
|
||||
}
|
||||
if (["png", "jpg", "jpeg", "gif", "svg", "webp", "ico"].includes(extension)) {
|
||||
return "imgs/[name][extname]";
|
||||
}
|
||||
return "otherassets/[name][extname]";
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
quietDeps: true,
|
||||
silenceDeprecations: ["import", "color-functions", "global-builtin"],
|
||||
verbose: false,
|
||||
importers: [new NodePackageImporter()],
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 1440,
|
||||
strictPort: true,
|
||||
host: devHost || false,
|
||||
ws: {
|
||||
protocol: "ws",
|
||||
host: devHost || "localhost",
|
||||
port: 1441,
|
||||
},
|
||||
watch: {
|
||||
ignored: ["**/src/**"],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -53,6 +53,7 @@
|
||||
],
|
||||
"resources": {
|
||||
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
|
||||
"../../config/composite.ksp-app-raw-transaction-ingest-desk.json": "config/composite.ksp-app-raw-transaction-ingest-desk.json",
|
||||
"../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-desk.json",
|
||||
"../../config/composite.ksp-app-store-desk.json": "config/composite.ksp-app-store-desk.json",
|
||||
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-solprices-desk/tests/desktop_contract.rs
|
||||
// version: 12
|
||||
// version: 13
|
||||
|
||||
//! Desktop scaffold, shared-template and Config packaging contract audits for SOL Prices Desk `0.2.12`.
|
||||
|
||||
@@ -85,17 +85,21 @@ fn pre_002_package_is_mixed_lib_bin_and_frontend_is_scaffold_only() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v0_3_8_pre_006_packaging_contains_current_fifteen_config_resources() {
|
||||
fn v0_3_8_pre_006_packaging_contains_current_sixteen_config_resources() {
|
||||
let root = app_root();
|
||||
let tauri = read_json(root.join("tauri.conf.json").as_path());
|
||||
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
|
||||
assert!(resources.is_some());
|
||||
if let std::option::Option::Some(resources) = resources {
|
||||
assert_eq!(resources.len(), 15);
|
||||
assert_eq!(resources.len(), 16);
|
||||
assert_eq!(
|
||||
resources.get("../../config/composite.ksp-app-backfill-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-backfill-desk.json"),
|
||||
);
|
||||
assert_eq!(
|
||||
resources.get("../../config/composite.ksp-app-raw-transaction-ingest-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-raw-transaction-ingest-desk.json"),
|
||||
);
|
||||
assert_eq!(
|
||||
resources.get("../../config/composite.ksp-app-solprices-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-solprices-desk.json"),
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
],
|
||||
"resources": {
|
||||
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
|
||||
"../../config/composite.ksp-app-raw-transaction-ingest-desk.json": "config/composite.ksp-app-raw-transaction-ingest-desk.json",
|
||||
"../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-desk.json",
|
||||
"../../config/composite.ksp-app-store-desk.json": "config/composite.ksp-app-store-desk.json",
|
||||
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-store-desk/tests/desktop_contract.rs
|
||||
// version: 10
|
||||
// version: 11
|
||||
|
||||
//! Desktop template and frontend contract tests for the Store Desk scaffold.
|
||||
|
||||
@@ -153,7 +153,8 @@ fn pre_006_composite_and_packaged_resources_cover_store_desk_and_the_complete_co
|
||||
let resources = tauri["bundle"]["resources"].as_object();
|
||||
assert!(resources.is_some());
|
||||
if let std::option::Option::Some(resources) = resources {
|
||||
assert_eq!(resources.len(), 15);
|
||||
assert_eq!(resources.len(), 16);
|
||||
assert!(resources.contains_key("../../config/composite.ksp-app-raw-transaction-ingest-desk.json"));
|
||||
assert!(resources.contains_key("../../config/composite.ksp-app-store-desk.json"));
|
||||
assert!(resources.contains_key("../../config/std.store.json"));
|
||||
assert!(resources.contains_key("../../config/schemas/std.store.schema.json"));
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
],
|
||||
"resources": {
|
||||
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
|
||||
"../../config/composite.ksp-app-raw-transaction-ingest-desk.json": "config/composite.ksp-app-raw-transaction-ingest-desk.json",
|
||||
"../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-desk.json",
|
||||
"../../config/composite.ksp-app-store-desk.json": "config/composite.ksp-app-store-desk.json",
|
||||
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs
|
||||
// version: 33
|
||||
// version: 34
|
||||
|
||||
//! Desktop build, shell and Config-status contract audits for Wallet Desk.
|
||||
|
||||
@@ -432,11 +432,15 @@ fn pre_018_packaged_runtime_bundles_config_resources_and_keeps_wallet_desk_versi
|
||||
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
|
||||
assert!(resources.is_some(), "packaged Wallet Desk Config resources map must exist");
|
||||
if let std::option::Option::Some(resources) = resources {
|
||||
assert_eq!(resources.len(), 15);
|
||||
assert_eq!(resources.len(), 16);
|
||||
assert_eq!(
|
||||
resources.get("../../config/composite.ksp-app-backfill-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-backfill-desk.json"),
|
||||
);
|
||||
assert_eq!(
|
||||
resources.get("../../config/composite.ksp-app-raw-transaction-ingest-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-raw-transaction-ingest-desk.json"),
|
||||
);
|
||||
assert_eq!(
|
||||
resources.get("../../config/composite.ksp-app-solprices-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-solprices-desk.json"),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/tests/release_compliance.rs
|
||||
// version: 12
|
||||
// version: 13
|
||||
|
||||
//! Release-wide deterministic compliance canaries for Wallet Desk.
|
||||
|
||||
@@ -199,7 +199,7 @@ fn packaged_resources_include_only_registered_config_sources_and_schemas() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert_eq!(resources.len(), 15);
|
||||
assert_eq!(resources.len(), 16);
|
||||
for (source, destination) in resources {
|
||||
let destination = destination.as_str();
|
||||
assert!(destination.is_some(), "resource destination must be textual");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-config-lib/README.md -->
|
||||
<!-- version: 12 -->
|
||||
<!-- version: 14 -->
|
||||
|
||||
# ksp-config-lib
|
||||
|
||||
@@ -36,6 +36,7 @@ Le registre par défaut connaît :
|
||||
|
||||
```text
|
||||
cfg.composite.ksp-app-backfill-desk -> config/composite.ksp-app-backfill-desk.json
|
||||
cfg.composite.ksp-app-raw-transaction-ingest-desk -> config/composite.ksp-app-raw-transaction-ingest-desk.json
|
||||
cfg.composite.ksp-app-solprices-desk -> config/composite.ksp-app-solprices-desk.json
|
||||
cfg.composite.ksp-app-store-desk -> config/composite.ksp-app-store-desk.json
|
||||
cfg.composite.ksp-app-wallet-desk -> config/composite.ksp-app-wallet-desk.json
|
||||
@@ -103,6 +104,7 @@ Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configur
|
||||
- [`../../config/std.store.json`](../../config/std.store.json) — targets Store PostgreSQL Devnet/Mainnet/Testnet et settings runtime bornés ;
|
||||
- [`../../config/std.wallet.json`](../../config/std.wallet.json) — racine Wallet globale et sous-répertoire optionnel par profil ;
|
||||
- [`../../config/composite.ksp-app-backfill-desk.json`](../../config/composite.ksp-app-backfill-desk.json) — composition Logging/Transport/Store de Backfill Desk ;
|
||||
- [`../../config/composite.ksp-app-raw-transaction-ingest-desk.json`](../../config/composite.ksp-app-raw-transaction-ingest-desk.json) — profils réseau-centriques Logging/Transport/Store du Raw Transaction Ingest Desk, capables d’agréger plusieurs sources Transport same-network ;
|
||||
- [`../../config/composite.ksp-app-solprices-desk.json`](../../config/composite.ksp-app-solprices-desk.json) — composition Logging/Off-chain Transport de SOL Prices Desk ;
|
||||
- [`../../config/composite.ksp-app-store-desk.json`](../../config/composite.ksp-app-store-desk.json) — composition Logging/Store de Store Desk ;
|
||||
- [`../../config/composite.ksp-app-wallet-desk.json`](../../config/composite.ksp-app-wallet-desk.json) — composition Logging/Off-chain Transport/On-chain Transport/Wallet de Wallet Desk ;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-config-lib/USAGE.md -->
|
||||
<!-- version: 15 -->
|
||||
<!-- version: 19 -->
|
||||
|
||||
# Utilisation de ksp-config-lib
|
||||
|
||||
@@ -28,6 +28,7 @@ Les arguments compris par Config sont :
|
||||
```text
|
||||
--cfgpath=/path/to/config
|
||||
--schemapath=/path/to/schemas
|
||||
--filemap=cfg.composite.ksp-app-raw-transaction-ingest-desk=my-raw-ingest-desk.json
|
||||
--filemap=cfg.composite.ksp-app-solprices-desk=my-solprices-desk.json
|
||||
--filemap=cfg.composite.ksp-app-wallet-desk=my-wallet-desk.json
|
||||
--filemap=cfg.std.logging=my-logging.json
|
||||
@@ -157,11 +158,13 @@ secret_metadata = metadata dont la valeur doit avoir une provenance KSP_SECRET_*
|
||||
|
||||
`protocol` n'est pas un nouveau `WsProtocolKind` et ne transforme pas PublicNode en protocole. Config résout les placeholders, vérifie la classe de sensibilité des metadata puis construit `YellowstoneGrpcTransportSettings`. Transport ne lit jamais l'environnement. Les valeurs de `secret_metadata` sont disponibles au runtime mais redacted dans `safe_value` et dans les représentations `Debug`.
|
||||
|
||||
Le provider affiche `solana-yellowstone-grpc.publicnode.com:443` ; le profil versionné `publicnode_mainnet` le représente sous la forme URL TLS requise par Transport, `https://solana-yellowstone-grpc.publicnode.com:443`, sans credential. Aucun hostname Testnet n'est versionné tant que sa valeur exacte n'est pas confirmée depuis une source opérateur/officielle.
|
||||
Les profils versionnés `publicnode_mainnet` et `publicnode_testnet` représentent leurs endpoints Yellowstone sous la forme URL TLS requise par Transport et conservent le `x-token` dans `secret_metadata`. Ils restent des sources Transport network-scoped et ne définissent jamais l’identité du réseau applicatif. `publicnode_mainnet` expose en plus le RPC HTTP Mainnet PublicNode utilisé par les consumers qui doivent hydrater une source Yellowstone same-provider ; ce rôle HTTP ne recopie pas une limite numérique Solana-public qui ne fait pas partie du contrat PublicNode committé.
|
||||
|
||||
L'accesseur historique `into_transport_settings()` conserve volontairement son tuple `(HTTP, Option<WS>)`. Un consumer ayant besoin des trois backends utilise `into_all_transport_settings()` ou les accesseurs séparés afin de ne pas casser silencieusement les consumers V2.
|
||||
|
||||
Les protocoles WebSocket restent `kind = "solana_standard"` et `kind = "helius_laserstream"`. Helius LaserStream WebSocket conserve ses URLs Config-owned et sa clé `KSP_SECRET_HELIUS_API_KEY`; cette surface est indépendante de Yellowstone gRPC.
|
||||
Les protocoles WebSocket restent `kind = "solana_standard"` et `kind = "helius_laserstream"`. Chaque endpoint peut en plus déclarer `capabilities`, liste de descriptors `WsSubscriptionKind` (`account`, `block`, `logs`, `program`, `root`, `signature`, `slot`, `slots_updates`, `vote`, `helius_transaction`). L'absence de cette liste reste backward-compatible mais ne constitue aucune preuve de support ; une liste présente est mappée explicitement vers Transport puis validée contre le protocole. Config ne déduit jamais une capability depuis `provider`.
|
||||
|
||||
Les profils Solana standard publics versionnés déclarent `account/block/logs/program/root/signature/slot`. `block` reste une capability explicite, instable et validator/provider-gated : Config ne l’infère jamais du protocol kind ni du provider, mais les endpoints publics committés l’annoncent désormais explicitement pour rendre la route Standard Block composable. Le profil `testnet_public` fournit le socle HTTP + WS standard Testnet sans credential ; `publicnode_testnet` reste une source Yellowstone optionnelle séparée. Les profils `helius_devnet` et `helius_mainnet` déclarent les sept familles Helius déjà supportées par la façade (`account/logs/program/root/signature/slot/slots_updates`) plus `helius_transaction`, sans `block` ni `vote`. Helius LaserStream WebSocket conserve ses URLs Config-owned et sa clé `KSP_SECRET_HELIUS_API_KEY`; cette surface est indépendante de Yellowstone gRPC.
|
||||
|
||||
Les scalaires `*_ms` restent des valeurs Config et sont convertis en `std::time::Duration` par l'adapter. La dépendance reste unidirectionnelle : Config connaît les contrats Transport pour les construire ; Transport ne connaît ni Config, ni `.env`, ni les variables KSP.
|
||||
|
||||
@@ -289,7 +292,7 @@ let component = match composite.component("wallet") {
|
||||
let wallet = engine.resolve_wallet_config_profile(component.resolved(), &environment);
|
||||
```
|
||||
|
||||
La même forme existe pour Logging via `resolve_logging_config_profile`. Le composite `cfg.composite.ksp-app-solprices-desk` référence `logging` et `offchain_transport`. Le composite `cfg.composite.ksp-app-wallet-desk` référence `logging`, `offchain_transport`, `transport` et `wallet`; chaque application valide ses frontières de composition au bootstrap.
|
||||
La même forme existe pour Logging via `resolve_logging_config_profile`. Le composite `cfg.composite.ksp-app-raw-transaction-ingest-desk` référence exactement `logging`, `transport` et `store`; ses profils couplent explicitement un profil Transport et un profil Store du même réseau logique (`devnet`, `mainnet` ou `testnet`) afin que l'application n'assemble jamais arbitrairement deux réseaux. Le composite `cfg.composite.ksp-app-solprices-desk` référence `logging` et `offchain_transport`. Le composite `cfg.composite.ksp-app-wallet-desk` référence `logging`, `offchain_transport`, `transport` et `wallet`; chaque application valide ses frontières de composition au bootstrap.
|
||||
|
||||
## 6. Management de `std.logging.json`
|
||||
|
||||
@@ -344,6 +347,8 @@ Elles n'altèrent jamais l'environnement hérité du processus. Une valeur proce
|
||||
|
||||
Sur Unix, un nouveau `.env` est créé avec des permissions privées `0600`; les permissions existantes sont préservées lors des remplacements atomiques.
|
||||
|
||||
Le composite `cfg.composite.ksp-app-raw-transaction-ingest-desk` expose des profils applicatifs `devnet`, `mainnet` et `testnet`. Un même profil peut référencer `transport`, `transport.helius` et `transport.yellowstone` tant que tous les profils Transport résolus appartiennent au même réseau que Store. Le provider identifie une source de capability, jamais un réseau logique.
|
||||
|
||||
## 8. `.env.example`
|
||||
|
||||
`/.env.example` est l'inventaire versionné. `/.env` reste local et ignoré.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/lib.rs
|
||||
// version: 23
|
||||
// version: 24
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -153,6 +153,8 @@ pub use self::registry::ConfigFileKind;
|
||||
pub use self::registry::ConfigFileRegistry;
|
||||
/// Default physical filename for the Backfill Desk composite configuration document.
|
||||
pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_BACKFILL_DESK_FILENAME;
|
||||
/// Default physical filename for the Raw Transaction Ingest Desk composite configuration document.
|
||||
pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK_FILENAME;
|
||||
/// Default physical filename for the SOL Prices Desk composite configuration document.
|
||||
pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME;
|
||||
/// Default physical filename for the Store Desk composite configuration document.
|
||||
@@ -183,6 +185,8 @@ pub use self::registry::DEFAULT_STD_WALLET_FILENAME;
|
||||
pub use self::registry::DEFAULT_STD_WALLET_SCHEMA_FILENAME;
|
||||
/// Logical file identifier for the Backfill Desk composite configuration document.
|
||||
pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK;
|
||||
/// Logical file identifier for the Raw Transaction Ingest Desk composite configuration document.
|
||||
pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK;
|
||||
/// Logical file identifier for the SOL Prices Desk composite configuration document.
|
||||
pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK;
|
||||
/// Logical file identifier for the Store Desk composite configuration document.
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// file: crates/ksp-config-lib/src/registry.rs
|
||||
// version: 15
|
||||
// version: 16
|
||||
|
||||
/// Bootstrap argument used to replace a known Config filename mapping.
|
||||
pub const ARG_FILE_MAP: &str = "--filemap";
|
||||
/// Default physical filename for the Backfill Desk composite configuration document.
|
||||
pub const DEFAULT_COMPOSITE_KSP_APP_BACKFILL_DESK_FILENAME: &str = "composite.ksp-app-backfill-desk.json";
|
||||
/// Default physical filename for the Raw Transaction Ingest Desk composite configuration document.
|
||||
pub const DEFAULT_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK_FILENAME: &str = "composite.ksp-app-raw-transaction-ingest-desk.json";
|
||||
/// Default physical filename for the SOL Prices Desk composite configuration document.
|
||||
pub const DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME: &str = "composite.ksp-app-solprices-desk.json";
|
||||
/// Default physical filename for the Store Desk composite configuration document.
|
||||
@@ -35,6 +37,8 @@ pub const DEFAULT_STD_WALLET_FILENAME: &str = "std.wallet.json";
|
||||
pub const DEFAULT_STD_WALLET_SCHEMA_FILENAME: &str = "std.wallet.schema.json";
|
||||
/// Logical file identifier for the Backfill Desk composite configuration document.
|
||||
pub const FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK: &str = "cfg.composite.ksp-app-backfill-desk";
|
||||
/// Logical file identifier for the Raw Transaction Ingest Desk composite configuration document.
|
||||
pub const FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK: &str = "cfg.composite.ksp-app-raw-transaction-ingest-desk";
|
||||
/// Logical file identifier for the SOL Prices Desk composite configuration document.
|
||||
pub const FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK: &str = "cfg.composite.ksp-app-solprices-desk";
|
||||
/// Logical file identifier for the Store Desk composite configuration document.
|
||||
@@ -178,6 +182,16 @@ impl ConfigFileRegistry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let raw_transaction_ingest_composite = ConfigFileDescriptor::new(
|
||||
FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK,
|
||||
ConfigFileKind::Config,
|
||||
DEFAULT_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK_FILENAME,
|
||||
std::option::Option::Some(FILE_ID_SCHEMA_COMPOSITE),
|
||||
);
|
||||
let raw_transaction_ingest_composite = match raw_transaction_ingest_composite {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let solprices_composite = ConfigFileDescriptor::new(
|
||||
FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK,
|
||||
ConfigFileKind::Config,
|
||||
@@ -300,6 +314,7 @@ impl ConfigFileRegistry {
|
||||
};
|
||||
return crate::build_registry([
|
||||
backfill_composite,
|
||||
raw_transaction_ingest_composite,
|
||||
solprices_composite,
|
||||
store_desk_composite,
|
||||
wallet_composite,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/transport.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
/// Effective standard on-chain Transport configuration resolved from Config and mapped to HTTP plus optional WebSocket and Yellowstone gRPC runtime contracts.
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
@@ -259,6 +259,8 @@ struct EffectiveWsEndpointSource {
|
||||
kind: String,
|
||||
url: String,
|
||||
#[serde(default)]
|
||||
capabilities: std::option::Option<std::vec::Vec<String>>,
|
||||
#[serde(default)]
|
||||
session: std::option::Option<EffectiveWsSessionOverrideSource>,
|
||||
}
|
||||
|
||||
@@ -812,7 +814,7 @@ fn map_ws_endpoints(
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
endpoints.push(ksp_onchain_transport_lib::WsEndpointSettings::new(
|
||||
let mut endpoint = ksp_onchain_transport_lib::WsEndpointSettings::new(
|
||||
source.name,
|
||||
source.enabled,
|
||||
ksp_onchain_transport_lib::WsProviderName::new(source.provider),
|
||||
@@ -820,7 +822,20 @@ fn map_ws_endpoints(
|
||||
protocol,
|
||||
url,
|
||||
session,
|
||||
));
|
||||
);
|
||||
if let std::option::Option::Some(capability_sources) = source.capabilities {
|
||||
let mut capabilities = std::vec::Vec::<ksp_onchain_transport_lib::WsSubscriptionKind>::with_capacity(capability_sources.len());
|
||||
for capability_source in capability_sources {
|
||||
let capability = map_ws_subscription_capability(capability_source.as_str(), profile, endpoint_name.as_str());
|
||||
let capability = match capability {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
capabilities.push(capability);
|
||||
}
|
||||
endpoint = endpoint.with_subscription_capabilities(capabilities);
|
||||
}
|
||||
endpoints.push(endpoint);
|
||||
}
|
||||
return std::result::Result::Ok(endpoints);
|
||||
}
|
||||
@@ -841,6 +856,30 @@ fn map_ws_protocol_kind(
|
||||
};
|
||||
}
|
||||
|
||||
fn map_ws_subscription_capability(
|
||||
value: &str,
|
||||
profile: &crate::ResolvedConfigProfile,
|
||||
endpoint_name: &str,
|
||||
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::WsSubscriptionKind> {
|
||||
return match value {
|
||||
"account" => std::result::Result::Ok(ksp_onchain_transport_lib::WsSubscriptionKind::Account),
|
||||
"block" => std::result::Result::Ok(ksp_onchain_transport_lib::WsSubscriptionKind::Block),
|
||||
"logs" => std::result::Result::Ok(ksp_onchain_transport_lib::WsSubscriptionKind::Logs),
|
||||
"program" => std::result::Result::Ok(ksp_onchain_transport_lib::WsSubscriptionKind::Program),
|
||||
"root" => std::result::Result::Ok(ksp_onchain_transport_lib::WsSubscriptionKind::Root),
|
||||
"signature" => std::result::Result::Ok(ksp_onchain_transport_lib::WsSubscriptionKind::Signature),
|
||||
"slot" => std::result::Result::Ok(ksp_onchain_transport_lib::WsSubscriptionKind::Slot),
|
||||
"slots_updates" => std::result::Result::Ok(ksp_onchain_transport_lib::WsSubscriptionKind::SlotsUpdates),
|
||||
"vote" => std::result::Result::Ok(ksp_onchain_transport_lib::WsSubscriptionKind::Vote),
|
||||
"helius_transaction" => std::result::Result::Ok(ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction),
|
||||
_ => std::result::Result::Err(
|
||||
effective_error(profile, "effective WebSocket subscription capability is unsupported")
|
||||
.with_context("endpoint_name", endpoint_name)
|
||||
.with_context("ws_capability", value),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn map_ws_session_settings(
|
||||
defaults: &EffectiveWsSessionSource,
|
||||
overrides: std::option::Option<&EffectiveWsSessionOverrideSource>,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/tests/ownership.rs
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
//! Workspace ownership audits for KSP application configuration boundaries.
|
||||
|
||||
@@ -262,6 +262,7 @@ fn workspace_crates_do_not_hardcode_config_managed_physical_files() {
|
||||
};
|
||||
for token in [
|
||||
"\".env\"",
|
||||
"\"composite.ksp-app-raw-transaction-ingest-desk.json\"",
|
||||
"\"composite.ksp-app-solprices-desk.json\"",
|
||||
"\"composite.ksp-app-wallet-desk.json\"",
|
||||
"\"composite.schema.json\"",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/tests/public_api.rs
|
||||
// version: 28
|
||||
// version: 29
|
||||
|
||||
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity,
|
||||
//! Logging/Transport/Store adapters and management contracts.
|
||||
@@ -80,23 +80,24 @@ fn registry_descriptor_inventory_is_available_from_crate_root() {
|
||||
assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}");
|
||||
if let std::result::Result::Ok(registry) = registry {
|
||||
let descriptors: std::vec::Vec<&ksp_config_lib::ConfigFileDescriptor> = registry.descriptors().collect();
|
||||
assert_eq!(descriptors.len(), 15);
|
||||
assert_eq!(descriptors.len(), 16);
|
||||
assert_eq!(descriptors[0].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK);
|
||||
assert_eq!(descriptors[1].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK);
|
||||
assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_STORE_DESK);
|
||||
assert_eq!(descriptors[3].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
|
||||
assert_eq!(descriptors[4].file_id().as_str(), ksp_config_lib::FILE_ID_STD_LOGGING);
|
||||
assert_eq!(descriptors[5].file_id().as_str(), ksp_config_lib::FILE_ID_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[6].file_id().as_str(), ksp_config_lib::FILE_ID_STD_STORE);
|
||||
assert_eq!(descriptors[7].file_id().as_str(), ksp_config_lib::FILE_ID_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[8].file_id().as_str(), ksp_config_lib::FILE_ID_STD_WALLET);
|
||||
assert_eq!(descriptors[9].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
|
||||
assert_eq!(descriptors[10].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
assert_eq!(descriptors[11].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[12].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_STORE);
|
||||
assert_eq!(descriptors[13].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[14].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
|
||||
let schema_file_id = descriptors[8].schema_file_id();
|
||||
assert_eq!(descriptors[1].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK);
|
||||
assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK);
|
||||
assert_eq!(descriptors[3].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_STORE_DESK);
|
||||
assert_eq!(descriptors[4].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
|
||||
assert_eq!(descriptors[5].file_id().as_str(), ksp_config_lib::FILE_ID_STD_LOGGING);
|
||||
assert_eq!(descriptors[6].file_id().as_str(), ksp_config_lib::FILE_ID_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[7].file_id().as_str(), ksp_config_lib::FILE_ID_STD_STORE);
|
||||
assert_eq!(descriptors[8].file_id().as_str(), ksp_config_lib::FILE_ID_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[9].file_id().as_str(), ksp_config_lib::FILE_ID_STD_WALLET);
|
||||
assert_eq!(descriptors[10].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
|
||||
assert_eq!(descriptors[11].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
assert_eq!(descriptors[12].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[13].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_STORE);
|
||||
assert_eq!(descriptors[14].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[15].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
|
||||
let schema_file_id = descriptors[9].schema_file_id();
|
||||
assert!(schema_file_id.is_some(), "public Wallet descriptor should preserve schema association");
|
||||
if let std::option::Option::Some(schema_file_id) = schema_file_id {
|
||||
assert_eq!(schema_file_id.as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
|
||||
@@ -167,6 +168,8 @@ fn composite_schema_and_provenance_contracts_are_available_from_crate_root() {
|
||||
}
|
||||
assert_eq!(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK, "cfg.composite.ksp-app-backfill-desk");
|
||||
assert_eq!(ksp_config_lib::DEFAULT_COMPOSITE_KSP_APP_BACKFILL_DESK_FILENAME, "composite.ksp-app-backfill-desk.json");
|
||||
assert_eq!(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK, "cfg.composite.ksp-app-raw-transaction-ingest-desk");
|
||||
assert_eq!(ksp_config_lib::DEFAULT_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK_FILENAME, "composite.ksp-app-raw-transaction-ingest-desk.json");
|
||||
assert_eq!(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK, "cfg.composite.ksp-app-solprices-desk");
|
||||
assert_eq!(ksp_config_lib::DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME, "composite.ksp-app-solprices-desk.json");
|
||||
assert_eq!(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_STORE_DESK, "cfg.composite.ksp-app-store-desk");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/composite.rs
|
||||
// version: 4
|
||||
// version: 6
|
||||
|
||||
const TEST_COMPOSITE_FILENAME: &str = "examples/composite.example.json";
|
||||
const TEST_COMPOSITE_FILE_ID: &str = "cfg.composite.test";
|
||||
@@ -128,6 +128,82 @@ fn duplicate_component_ids_are_rejected_inside_one_composite_profile() {
|
||||
cleanup_fixture(&fixture);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v0_3_15_pre_006_fix_001_raw_ingest_composite_is_network_centric_and_keeps_all_transport_sources_coherent() {
|
||||
let workspace = workspace_root();
|
||||
let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
|
||||
assert!(bootstrap.is_ok(), "committed Config roots should be valid: {bootstrap:?}");
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
assert!(registry.is_ok(), "default Config registry should be valid: {registry:?}");
|
||||
let file_id = crate::ConfigFileId::new(crate::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK);
|
||||
assert!(file_id.is_ok(), "Raw Transaction Ingest Desk composite file_id should be valid: {file_id:?}");
|
||||
let (bootstrap, registry, file_id) = match (bootstrap, registry, file_id) {
|
||||
(std::result::Result::Ok(bootstrap), std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) => (bootstrap, registry, file_id),
|
||||
_ => return,
|
||||
};
|
||||
let engine = crate::ConfigDocumentEngine::new(bootstrap, registry);
|
||||
let mut process = std::collections::BTreeMap::<String, String>::new();
|
||||
process.insert("KSP_SECRET_HELIUS_API_KEY".to_owned(), "helius-composite-canary".to_owned());
|
||||
process.insert("KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN".to_owned(), "orbitflare-composite-canary".to_owned());
|
||||
process.insert("KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN".to_owned(), "publicnode-mainnet-composite-canary".to_owned());
|
||||
process.insert("KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN".to_owned(), "publicnode-testnet-composite-canary".to_owned());
|
||||
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||
for (profile_id, expected_network, expected_transport_profiles) in [
|
||||
("devnet", "devnet", vec!["devnet_public", "helius_devnet", "orbitflare_devnet"]),
|
||||
("mainnet", "mainnet", vec!["mainnet_public", "helius_mainnet", "publicnode_mainnet"]),
|
||||
("testnet", "testnet", vec!["testnet_public", "publicnode_testnet"]),
|
||||
] {
|
||||
let composite = engine.load_resolved_composite(&file_id, std::option::Option::Some(profile_id));
|
||||
assert!(composite.is_ok(), "Raw Ingest network profile {profile_id} should resolve: {composite:?}");
|
||||
let composite = match composite {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => continue,
|
||||
};
|
||||
assert_eq!(composite.profile_id(), profile_id);
|
||||
let store_component = composite.component("store");
|
||||
assert!(store_component.is_some(), "profile {profile_id} should contain Store");
|
||||
let store_component = match store_component {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
assert_eq!(store_component.resolved().profile_id(), expected_network);
|
||||
let store = engine.resolve_store_config_profile(store_component.resolved(), &environment);
|
||||
assert!(store.is_ok(), "Store profile {expected_network} should map: {store:?}");
|
||||
if let std::result::Result::Ok(store) = store {
|
||||
assert_eq!(store.settings().network().as_str(), expected_network);
|
||||
}
|
||||
let transport_components = composite
|
||||
.components()
|
||||
.iter()
|
||||
.filter(|(component_id, component)| {
|
||||
return component_id.starts_with("transport") && component.resolved().file_id().as_str() == crate::FILE_ID_STD_TRANSPORT;
|
||||
})
|
||||
.map(|(_, component)| return component)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(transport_components.len(), expected_transport_profiles.len());
|
||||
let actual_profiles = transport_components.iter().map(|component| return component.resolved().profile_id()).collect::<std::vec::Vec<_>>();
|
||||
for expected_transport_profile in expected_transport_profiles {
|
||||
assert!(
|
||||
actual_profiles.contains(&expected_transport_profile),
|
||||
"network profile {profile_id} should include Transport source {expected_transport_profile}"
|
||||
);
|
||||
}
|
||||
for component in transport_components {
|
||||
let transport = engine.resolve_transport_config_profile(component.resolved(), &environment);
|
||||
assert!(transport.is_ok(), "Transport source {} should map: {transport:?}", component.resolved().profile_id());
|
||||
if let std::result::Result::Ok(transport) = transport {
|
||||
assert!(transport.http_settings().endpoints().iter().all(|endpoint| return endpoint.cluster().as_str() == expected_network));
|
||||
if let std::option::Option::Some(ws) = transport.ws_settings() {
|
||||
assert!(ws.endpoints().iter().all(|endpoint| return endpoint.cluster().as_str() == expected_network));
|
||||
}
|
||||
if let std::option::Option::Some(grpc) = transport.grpc_settings() {
|
||||
assert!(grpc.endpoints().iter().all(|endpoint| return endpoint.cluster().as_str() == expected_network));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fixture_example_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
|
||||
let workspace = workspace_root();
|
||||
let fixture_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("unit_tests/fixtures");
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"default_profile": "devnet",
|
||||
"profiles": [
|
||||
{
|
||||
"profile_id": "devnet",
|
||||
"documents": [
|
||||
{
|
||||
"component_id": "logging",
|
||||
"file_id": "cfg.std.logging",
|
||||
"profile_id": "local_dev"
|
||||
},
|
||||
{
|
||||
"component_id": "transport",
|
||||
"file_id": "cfg.std.transport",
|
||||
"profile_id": "secret_test"
|
||||
},
|
||||
{
|
||||
"component_id": "transport.helius",
|
||||
"file_id": "cfg.std.transport",
|
||||
"profile_id": "helius_devnet"
|
||||
},
|
||||
{
|
||||
"component_id": "transport.yellowstone",
|
||||
"file_id": "cfg.std.transport",
|
||||
"profile_id": "secret_test"
|
||||
},
|
||||
{
|
||||
"component_id": "store",
|
||||
"file_id": "cfg.std.store",
|
||||
"profile_id": "devnet"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"profile_id": "mainnet",
|
||||
"documents": [
|
||||
{
|
||||
"component_id": "logging",
|
||||
"file_id": "cfg.std.logging",
|
||||
"profile_id": "local_dev"
|
||||
},
|
||||
{
|
||||
"component_id": "transport",
|
||||
"file_id": "cfg.std.transport",
|
||||
"profile_id": "secret_test"
|
||||
},
|
||||
{
|
||||
"component_id": "transport.helius",
|
||||
"file_id": "cfg.std.transport",
|
||||
"profile_id": "helius_devnet"
|
||||
},
|
||||
{
|
||||
"component_id": "transport.yellowstone",
|
||||
"file_id": "cfg.std.transport",
|
||||
"profile_id": "secret_test"
|
||||
},
|
||||
{
|
||||
"component_id": "store",
|
||||
"file_id": "cfg.std.store",
|
||||
"profile_id": "mainnet"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"profile_id": "testnet",
|
||||
"documents": [
|
||||
{
|
||||
"component_id": "logging",
|
||||
"file_id": "cfg.std.logging",
|
||||
"profile_id": "local_dev"
|
||||
},
|
||||
{
|
||||
"component_id": "transport",
|
||||
"file_id": "cfg.std.transport",
|
||||
"profile_id": "secret_test"
|
||||
},
|
||||
{
|
||||
"component_id": "transport.yellowstone",
|
||||
"file_id": "cfg.std.transport",
|
||||
"profile_id": "secret_test"
|
||||
},
|
||||
{
|
||||
"component_id": "store",
|
||||
"file_id": "cfg.std.store",
|
||||
"profile_id": "testnet"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -70,7 +70,15 @@
|
||||
"resubscribe": "never",
|
||||
"notification_queue_capacity": 48,
|
||||
"max_pending_requests": 24
|
||||
}
|
||||
},
|
||||
"capabilities": [
|
||||
"account",
|
||||
"logs",
|
||||
"program",
|
||||
"root",
|
||||
"signature",
|
||||
"slot"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "fixture_helius_ws",
|
||||
@@ -78,7 +86,17 @@
|
||||
"provider": "helius",
|
||||
"cluster": "mainnet",
|
||||
"kind": "helius_laserstream",
|
||||
"url": "wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-fixture-helius-key}"
|
||||
"url": "wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-fixture-helius-key}",
|
||||
"capabilities": [
|
||||
"account",
|
||||
"logs",
|
||||
"program",
|
||||
"root",
|
||||
"signature",
|
||||
"slot",
|
||||
"slots_updates",
|
||||
"helius_transaction"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -119,7 +137,17 @@
|
||||
"provider": "helius",
|
||||
"cluster": "devnet",
|
||||
"kind": "helius_laserstream",
|
||||
"url": "wss://devnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-fixture-helius-key}"
|
||||
"url": "wss://devnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-fixture-helius-key}",
|
||||
"capabilities": [
|
||||
"account",
|
||||
"logs",
|
||||
"program",
|
||||
"root",
|
||||
"signature",
|
||||
"slot",
|
||||
"slots_updates",
|
||||
"helius_transaction"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/registry.rs
|
||||
// version: 13
|
||||
// version: 14
|
||||
|
||||
#[test]
|
||||
fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
|
||||
@@ -7,30 +7,33 @@ fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
|
||||
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
|
||||
if let std::result::Result::Ok(registry) = registry {
|
||||
let descriptors: std::vec::Vec<&crate::ConfigFileDescriptor> = registry.descriptors().collect();
|
||||
assert_eq!(descriptors.len(), 15);
|
||||
assert_eq!(descriptors.len(), 16);
|
||||
assert_eq!(descriptors[0].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK);
|
||||
assert_eq!(descriptors[0].filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_BACKFILL_DESK_FILENAME));
|
||||
assert_eq!(descriptors[0].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_COMPOSITE));
|
||||
assert_eq!(descriptors[1].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK);
|
||||
assert_eq!(descriptors[2].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_STORE_DESK);
|
||||
assert_eq!(descriptors[2].filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_STORE_DESK_FILENAME));
|
||||
assert_eq!(descriptors[2].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_COMPOSITE));
|
||||
assert_eq!(descriptors[3].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
|
||||
assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_STD_LOGGING);
|
||||
assert_eq!(descriptors[5].file_id().as_str(), crate::FILE_ID_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[6].file_id().as_str(), crate::FILE_ID_STD_STORE);
|
||||
assert_eq!(descriptors[7].file_id().as_str(), crate::FILE_ID_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[8].file_id().as_str(), crate::FILE_ID_STD_WALLET);
|
||||
assert_eq!(descriptors[8].filename(), std::path::Path::new(crate::DEFAULT_STD_WALLET_FILENAME));
|
||||
assert_eq!(descriptors[8].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_STD_WALLET));
|
||||
assert_eq!(descriptors[9].file_id().as_str(), crate::FILE_ID_SCHEMA_COMPOSITE);
|
||||
assert_eq!(descriptors[10].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
assert_eq!(descriptors[11].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[12].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_STORE);
|
||||
assert_eq!(descriptors[13].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[14].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_WALLET);
|
||||
assert!(descriptors[0..9].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Config));
|
||||
assert!(descriptors[9..15].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Schema));
|
||||
assert_eq!(descriptors[1].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK);
|
||||
assert_eq!(descriptors[1].filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK_FILENAME));
|
||||
assert_eq!(descriptors[1].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_COMPOSITE));
|
||||
assert_eq!(descriptors[2].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK);
|
||||
assert_eq!(descriptors[3].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_STORE_DESK);
|
||||
assert_eq!(descriptors[3].filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_STORE_DESK_FILENAME));
|
||||
assert_eq!(descriptors[3].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_COMPOSITE));
|
||||
assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
|
||||
assert_eq!(descriptors[5].file_id().as_str(), crate::FILE_ID_STD_LOGGING);
|
||||
assert_eq!(descriptors[6].file_id().as_str(), crate::FILE_ID_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[7].file_id().as_str(), crate::FILE_ID_STD_STORE);
|
||||
assert_eq!(descriptors[8].file_id().as_str(), crate::FILE_ID_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[9].file_id().as_str(), crate::FILE_ID_STD_WALLET);
|
||||
assert_eq!(descriptors[9].filename(), std::path::Path::new(crate::DEFAULT_STD_WALLET_FILENAME));
|
||||
assert_eq!(descriptors[9].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_STD_WALLET));
|
||||
assert_eq!(descriptors[10].file_id().as_str(), crate::FILE_ID_SCHEMA_COMPOSITE);
|
||||
assert_eq!(descriptors[11].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
assert_eq!(descriptors[12].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[13].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_STORE);
|
||||
assert_eq!(descriptors[14].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[15].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_WALLET);
|
||||
assert!(descriptors[0..10].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Config));
|
||||
assert!(descriptors[10..16].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Schema));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,16 +368,18 @@ fn absolute_fixture_path() -> std::path::PathBuf {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_register_generic_composite_schema_and_four_runtime_composites() {
|
||||
fn defaults_register_generic_composite_schema_and_five_runtime_composites() {
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
let schema_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_COMPOSITE);
|
||||
let backfill_id = crate::ConfigFileId::new(crate::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK);
|
||||
let raw_ingest_id = crate::ConfigFileId::new(crate::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK);
|
||||
let solprices_id = crate::ConfigFileId::new(crate::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK);
|
||||
let store_desk_id = crate::ConfigFileId::new(crate::FILE_ID_COMPOSITE_KSP_APP_STORE_DESK);
|
||||
let wallet_id = crate::ConfigFileId::new(crate::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
|
||||
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
|
||||
assert!(schema_id.is_ok(), "composite schema file_id should be valid: {schema_id:?}");
|
||||
assert!(backfill_id.is_ok(), "Backfill Desk composite runtime file_id should be valid: {backfill_id:?}");
|
||||
assert!(raw_ingest_id.is_ok(), "Raw Transaction Ingest Desk composite runtime file_id should be valid: {raw_ingest_id:?}");
|
||||
assert!(solprices_id.is_ok(), "SOL Prices Desk composite runtime file_id should be valid: {solprices_id:?}");
|
||||
assert!(store_desk_id.is_ok(), "Store Desk composite runtime file_id should be valid: {store_desk_id:?}");
|
||||
assert!(wallet_id.is_ok(), "Wallet Desk composite runtime file_id should be valid: {wallet_id:?}");
|
||||
@@ -382,34 +387,41 @@ fn defaults_register_generic_composite_schema_and_four_runtime_composites() {
|
||||
std::result::Result::Ok(registry),
|
||||
std::result::Result::Ok(schema_id),
|
||||
std::result::Result::Ok(backfill_id),
|
||||
std::result::Result::Ok(raw_ingest_id),
|
||||
std::result::Result::Ok(solprices_id),
|
||||
std::result::Result::Ok(store_desk_id),
|
||||
std::result::Result::Ok(wallet_id),
|
||||
) = (registry, schema_id, backfill_id, solprices_id, store_desk_id, wallet_id)
|
||||
) = (registry, schema_id, backfill_id, raw_ingest_id, solprices_id, store_desk_id, wallet_id)
|
||||
{
|
||||
let schema = registry.descriptor(&schema_id);
|
||||
let backfill = registry.descriptor(&backfill_id);
|
||||
let raw_ingest = registry.descriptor(&raw_ingest_id);
|
||||
let solprices = registry.descriptor(&solprices_id);
|
||||
let store_desk = registry.descriptor(&store_desk_id);
|
||||
let wallet = registry.descriptor(&wallet_id);
|
||||
assert!(schema.is_ok(), "generic composite schema should be registered: {schema:?}");
|
||||
assert!(backfill.is_ok(), "Backfill Desk runtime composite should be registered: {backfill:?}");
|
||||
assert!(raw_ingest.is_ok(), "Raw Transaction Ingest Desk runtime composite should be registered: {raw_ingest:?}");
|
||||
assert!(solprices.is_ok(), "SOL Prices Desk runtime composite should be registered: {solprices:?}");
|
||||
assert!(store_desk.is_ok(), "Store Desk runtime composite should be registered: {store_desk:?}");
|
||||
assert!(wallet.is_ok(), "Wallet Desk runtime composite should be registered: {wallet:?}");
|
||||
if let (
|
||||
std::result::Result::Ok(schema),
|
||||
std::result::Result::Ok(backfill),
|
||||
std::result::Result::Ok(raw_ingest),
|
||||
std::result::Result::Ok(solprices),
|
||||
std::result::Result::Ok(store_desk),
|
||||
std::result::Result::Ok(wallet),
|
||||
) = (schema, backfill, solprices, store_desk, wallet)
|
||||
) = (schema, backfill, raw_ingest, solprices, store_desk, wallet)
|
||||
{
|
||||
assert_eq!(schema.kind(), crate::ConfigFileKind::Schema);
|
||||
assert_eq!(schema.filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_SCHEMA_FILENAME));
|
||||
assert_eq!(backfill.kind(), crate::ConfigFileKind::Config);
|
||||
assert_eq!(backfill.filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_BACKFILL_DESK_FILENAME));
|
||||
assert_eq!(backfill.schema_file_id(), std::option::Option::Some(&schema_id));
|
||||
assert_eq!(raw_ingest.kind(), crate::ConfigFileKind::Config);
|
||||
assert_eq!(raw_ingest.filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK_FILENAME));
|
||||
assert_eq!(raw_ingest.schema_file_id(), std::option::Option::Some(&schema_id));
|
||||
assert_eq!(solprices.kind(), crate::ConfigFileKind::Config);
|
||||
assert_eq!(solprices.filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME));
|
||||
assert_eq!(solprices.schema_file_id(), std::option::Option::Some(&schema_id));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/transport.rs
|
||||
// version: 13
|
||||
// version: 18
|
||||
|
||||
#[test]
|
||||
fn fixture_transport_profile_maps_complete_runtime_contract() {
|
||||
@@ -51,6 +51,19 @@ fn fixture_transport_profile_maps_complete_runtime_contract() {
|
||||
assert_eq!(endpoint.provider().as_str(), "fixture-provider");
|
||||
assert_eq!(endpoint.cluster().as_str(), "fixture-cluster");
|
||||
assert_eq!(endpoint.protocol(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard);
|
||||
assert_eq!(
|
||||
endpoint.subscription_capabilities(),
|
||||
std::option::Option::Some(
|
||||
&[
|
||||
ksp_onchain_transport_lib::WsSubscriptionKind::Account,
|
||||
ksp_onchain_transport_lib::WsSubscriptionKind::Logs,
|
||||
ksp_onchain_transport_lib::WsSubscriptionKind::Program,
|
||||
ksp_onchain_transport_lib::WsSubscriptionKind::Root,
|
||||
ksp_onchain_transport_lib::WsSubscriptionKind::Signature,
|
||||
ksp_onchain_transport_lib::WsSubscriptionKind::Slot,
|
||||
][..],
|
||||
)
|
||||
);
|
||||
assert_eq!(endpoint.url().as_str(), "wss://fallback.invalid");
|
||||
assert_eq!(endpoint.session().command_timeout(), std::time::Duration::from_millis(4500));
|
||||
assert_eq!(endpoint.session().close_timeout(), std::time::Duration::from_millis(4000));
|
||||
@@ -70,6 +83,21 @@ fn fixture_transport_profile_maps_complete_runtime_contract() {
|
||||
assert_eq!(helius.provider().as_str(), "helius");
|
||||
assert_eq!(helius.cluster().as_str(), "mainnet");
|
||||
assert_eq!(helius.protocol(), ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream);
|
||||
assert_eq!(
|
||||
helius.subscription_capabilities(),
|
||||
std::option::Option::Some(
|
||||
&[
|
||||
ksp_onchain_transport_lib::WsSubscriptionKind::Account,
|
||||
ksp_onchain_transport_lib::WsSubscriptionKind::Logs,
|
||||
ksp_onchain_transport_lib::WsSubscriptionKind::Program,
|
||||
ksp_onchain_transport_lib::WsSubscriptionKind::Root,
|
||||
ksp_onchain_transport_lib::WsSubscriptionKind::Signature,
|
||||
ksp_onchain_transport_lib::WsSubscriptionKind::Slot,
|
||||
ksp_onchain_transport_lib::WsSubscriptionKind::SlotsUpdates,
|
||||
ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction,
|
||||
][..],
|
||||
)
|
||||
);
|
||||
assert_eq!(helius.url().as_str(), "wss://mainnet.helius-rpc.com/?api-key=fixture-helius-key");
|
||||
let _connect_future = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::connect(helius.clone());
|
||||
}
|
||||
@@ -117,6 +145,8 @@ fn committed_transport_document_maps_default_and_explicit_profiles() {
|
||||
if let std::option::Option::Some(ws) = ws {
|
||||
assert_eq!(ws.endpoints()[0].url().as_str(), "wss://api.devnet.solana.com");
|
||||
assert_eq!(ws.endpoints()[0].protocol(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard);
|
||||
assert!(ws.endpoints()[0].supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Logs));
|
||||
assert!(ws.endpoints()[0].supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Block));
|
||||
}
|
||||
}
|
||||
if let std::result::Result::Ok(mainnet) = mainnet {
|
||||
@@ -129,6 +159,8 @@ fn committed_transport_document_maps_default_and_explicit_profiles() {
|
||||
assert!(ws.is_some(), "committed V3 Mainnet profile should expose WebSocket settings");
|
||||
if let std::option::Option::Some(ws) = ws {
|
||||
assert_eq!(ws.endpoints()[0].url().as_str(), "wss://api.mainnet.solana.com");
|
||||
assert!(ws.endpoints()[0].supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Logs));
|
||||
assert!(ws.endpoints()[0].supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Block));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,6 +211,33 @@ fn committed_v3_orbitflare_devnet_profile_maps_provider_neutral_yellowstone_grpc
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v0_3_15_pre_006_fix_001_testnet_public_is_keyless_standard_transport_without_yellowstone() {
|
||||
let engine = committed_engine();
|
||||
let engine = match engine {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||
let resolved = engine.load_resolved_transport_config(std::option::Option::Some("testnet_public"), &environment);
|
||||
assert!(resolved.is_ok(), "committed keyless Testnet Transport profile should map without provider credentials: {resolved:?}");
|
||||
if let std::result::Result::Ok(resolved) = resolved {
|
||||
assert_eq!(resolved.profile_id(), "testnet_public");
|
||||
assert_eq!(resolved.settings().endpoints().len(), 1);
|
||||
assert_eq!(resolved.settings().endpoints()[0].cluster().as_str(), "testnet");
|
||||
let ws = resolved.ws_settings();
|
||||
assert!(ws.is_some());
|
||||
if let std::option::Option::Some(ws) = ws {
|
||||
assert_eq!(ws.endpoints().len(), 1);
|
||||
assert_eq!(ws.endpoints()[0].cluster().as_str(), "testnet");
|
||||
assert_eq!(ws.endpoints()[0].protocol(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard);
|
||||
assert!(ws.endpoints()[0].supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Logs));
|
||||
assert!(ws.endpoints()[0].supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Block));
|
||||
}
|
||||
assert!(resolved.grpc_settings().is_none(), "keyless Testnet base profile must not conflate PublicNode Yellowstone with the network itself");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn committed_v3_publicnode_profiles_map_provider_neutral_yellowstone_grpc_with_network_scoped_secret_x_tokens() {
|
||||
let engine = committed_engine();
|
||||
@@ -214,6 +273,27 @@ fn committed_v3_publicnode_profiles_map_provider_neutral_yellowstone_grpc_with_n
|
||||
std::result::Result::Err(_) => continue,
|
||||
};
|
||||
assert_eq!(resolved.profile_id(), profile_id);
|
||||
if profile_id == "publicnode_mainnet" {
|
||||
assert_eq!(resolved.settings().endpoints().len(), 2);
|
||||
let publicnode = &resolved.settings().endpoints()[0];
|
||||
assert_eq!(publicnode.name(), "publicnode_solana_mainnet_rpc");
|
||||
assert_eq!(publicnode.provider().as_str(), "publicnode");
|
||||
assert_eq!(publicnode.cluster().as_str(), "mainnet");
|
||||
assert_eq!(publicnode.url().as_str(), "https://solana-rpc.publicnode.com");
|
||||
assert_eq!(publicnode.roles().len(), 1);
|
||||
let limits = publicnode.roles()[0].limits();
|
||||
assert!(limits.requests_per_second().is_none());
|
||||
assert!(limits.burst_capacity().is_none());
|
||||
assert_eq!(limits.max_concurrent_requests().map(std::num::NonZeroU32::get), std::option::Option::Some(8));
|
||||
let solana_public = &resolved.settings().endpoints()[1];
|
||||
assert_eq!(solana_public.name(), "solana_mainnet_public_repair");
|
||||
assert_eq!(solana_public.provider().as_str(), "solana-public");
|
||||
assert_eq!(solana_public.cluster().as_str(), "mainnet");
|
||||
assert_eq!(solana_public.roles()[0].role().as_str(), "default");
|
||||
let solana_limits = solana_public.roles()[0].limits();
|
||||
assert_eq!(solana_limits.requests_per_second().map(std::num::NonZeroU32::get), std::option::Option::Some(5));
|
||||
assert_eq!(solana_limits.burst_capacity().map(std::num::NonZeroU32::get), std::option::Option::Some(10));
|
||||
}
|
||||
assert!(resolved.ws_settings().is_some());
|
||||
let grpc = resolved.grpc_settings();
|
||||
assert!(grpc.is_some(), "PublicNode profile {profile_id} should expose Yellowstone gRPC settings");
|
||||
@@ -529,6 +609,76 @@ fn helius_laserstream_mainnet_and_devnet_api_key_map_to_protocol_and_safe_redact
|
||||
assert!(devnet_debug.contains(crate::REDACTED_CONFIG_VALUE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v0_3_15_pre_006_fix_002_committed_public_standard_profiles_declare_block_capability() {
|
||||
let engine = committed_engine();
|
||||
let engine = match engine {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||
for (profile_id, cluster) in [("devnet_public", "devnet"), ("mainnet_public", "mainnet"), ("testnet_public", "testnet")] {
|
||||
let resolved = engine.load_resolved_transport_config(std::option::Option::Some(profile_id), &environment);
|
||||
assert!(resolved.is_ok(), "committed public standard profile {profile_id} should resolve: {resolved:?}");
|
||||
let resolved = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => continue,
|
||||
};
|
||||
let ws = resolved.ws_settings();
|
||||
assert!(ws.is_some(), "committed public standard profile {profile_id} should expose WebSocket settings");
|
||||
if let std::option::Option::Some(ws) = ws {
|
||||
assert_eq!(ws.endpoints().len(), 1);
|
||||
let endpoint = &ws.endpoints()[0];
|
||||
assert_eq!(endpoint.provider().as_str(), "solana-public");
|
||||
assert_eq!(endpoint.cluster().as_str(), cluster);
|
||||
assert_eq!(endpoint.protocol(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard);
|
||||
assert!(endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Logs));
|
||||
assert!(endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Block));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v0_3_15_pre_003_committed_helius_profiles_map_transaction_capability_without_block_or_vote() {
|
||||
let engine = committed_engine();
|
||||
let engine = match engine {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let canary = "helius-pre-003-api-key-canary";
|
||||
let mut process = std::collections::BTreeMap::<String, String>::new();
|
||||
process.insert("KSP_SECRET_HELIUS_API_KEY".to_owned(), canary.to_owned());
|
||||
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||
for (profile_id, cluster, expected_url) in [
|
||||
("helius_devnet", "devnet", "wss://devnet.helius-rpc.com/?api-key=helius-pre-003-api-key-canary"),
|
||||
("helius_mainnet", "mainnet", "wss://mainnet.helius-rpc.com/?api-key=helius-pre-003-api-key-canary"),
|
||||
] {
|
||||
let resolved = engine.load_resolved_transport_config(std::option::Option::Some(profile_id), &environment);
|
||||
assert!(resolved.is_ok(), "committed Helius profile {profile_id} should resolve: {resolved:?}");
|
||||
let resolved = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => continue,
|
||||
};
|
||||
assert_eq!(resolved.http_settings().endpoints()[0].cluster().as_str(), cluster);
|
||||
let ws = resolved.ws_settings();
|
||||
assert!(ws.is_some(), "committed Helius profile {profile_id} should expose WebSocket settings");
|
||||
if let std::option::Option::Some(ws) = ws {
|
||||
assert_eq!(ws.endpoints().len(), 1);
|
||||
let endpoint = &ws.endpoints()[0];
|
||||
assert_eq!(endpoint.provider().as_str(), "helius");
|
||||
assert_eq!(endpoint.cluster().as_str(), cluster);
|
||||
assert_eq!(endpoint.protocol(), ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream);
|
||||
assert_eq!(endpoint.url().as_str(), expected_url);
|
||||
assert!(endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Logs));
|
||||
assert!(endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction));
|
||||
assert!(!endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Block));
|
||||
assert!(!endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Vote));
|
||||
}
|
||||
let debug = format!("{resolved:?}");
|
||||
assert!(!debug.contains(canary));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_secret_url_provenance_uses_process_and_process_beats_dotenv() {
|
||||
let engine = fixture_engine();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-onchain-transport-lib/README.md -->
|
||||
<!-- version: 24 -->
|
||||
<!-- version: 26 -->
|
||||
|
||||
# `ksp-onchain-transport-lib`
|
||||
|
||||
@@ -239,7 +239,9 @@ YellowstoneGrpcSubscribeSnapshot
|
||||
|
||||
Les sept unary standards retenus sont `SubscribeReplayInfo`, `Ping`, `GetLatestBlockhash`, `GetBlockHeight`, `GetSlot`, `IsBlockhashValid` et `GetVersion`. `Subscribe` couvre accounts, slots, transactions, transaction status, blocks, block metadata et entries, avec `commitment`, `accounts_data_slice`, `ping` et `from_slot`. `SubscribeDeshred` reste hors scope de la foundation standard.
|
||||
|
||||
Le stream bidirectionnel est borné : request/update queues, tailles inbound/outbound, half-close, close timeout et reconnect budget sont explicites. Après reconnect, KSP rejoue la dernière requête complète acceptée et avance prudemment `from_slot` selon le dernier slot observé et `SubscribeReplayInfo.first_available`. Les compteurs de gap et duplicate sont de l’observabilité ; ils ne constituent jamais une garantie exactly-once ou lossless.
|
||||
Le stream bidirectionnel est borné : request/update queues, tailles inbound/outbound, half-close, close timeout et reconnect budget sont explicites. La queue d'updates Yellowstone reste bornée mais sa saturation locale n'est plus un fault : l'acteur attend asynchronement qu'une place soit rendue et cesse donc de poller le stream Tonic, ce qui propage la backpressure via gRPC/HTTP/2 sans drop silencieux ni queue non bornée. Le shutdown préempte cette attente. La queue de mutations de requête reste au contraire synchrone/fail-fast : un caller qui la sature reçoit toujours `grpc_backpressure_overflow`. Pendant un shutdown local explicite, un `Status` gRPC reçu après le half-close client est traité comme une terminaison `Closed` de cette fermeture déjà engagée ; le même `Status` reçu pendant une session active conserve son comportement normal de reconnect/fault et n'est pas masqué.
|
||||
|
||||
Après reconnect, KSP rejoue la dernière requête complète acceptée et avance prudemment `from_slot` selon le dernier slot observé et `SubscribeReplayInfo.first_available`. Les compteurs de gap et duplicate sont de l’observabilité ; ils ne constituent jamais une garantie exactly-once ou lossless.
|
||||
|
||||
Config Transport V3 peut mapper des `grpc_endpoints` vers ces settings sans inverser la dépendance. `protocol = solana_yellowstone` décrit le wire standard, tandis que `provider` reste un descripteur distinct. Les profils committés `publicnode_mainnet` et `publicnode_testnet` utilisent le standard sans façade PublicNode spécifique. Leur personal token est injecté comme metadata secrète `x-token` depuis Config ; Transport ne lit jamais les variables KSP.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-onchain-transport-lib/USAGE.md -->
|
||||
<!-- version: 25 -->
|
||||
<!-- version: 27 -->
|
||||
|
||||
# Utilisation de `ksp-onchain-transport-lib`
|
||||
|
||||
@@ -82,7 +82,14 @@ let endpoint = ksp_onchain_transport_lib::WsEndpointSettings::new(
|
||||
ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard,
|
||||
ws_url,
|
||||
ksp_onchain_transport_lib::WsSessionSettings::default(),
|
||||
);
|
||||
)
|
||||
.with_subscription_capabilities(vec![ksp_onchain_transport_lib::WsSubscriptionKind::Logs]);
|
||||
if !endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Logs) {
|
||||
return Err(ksp_core_lib::Error::new(
|
||||
ksp_onchain_transport_lib::ERROR_CODE_INVALID_SETTINGS,
|
||||
"required WebSocket subscription capability is not declared",
|
||||
));
|
||||
}
|
||||
let session = match ksp_onchain_transport_lib::WsSession::connect(endpoint).await {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
@@ -90,6 +97,8 @@ let session = match ksp_onchain_transport_lib::WsSession::connect(endpoint).awai
|
||||
let snapshot = session.snapshot();
|
||||
```
|
||||
|
||||
`with_subscription_capabilities(...)` porte une preuve explicite et typed des familles déclarées pour cet endpoint. Une déclaration vide, dupliquée ou incompatible avec `WsProtocolKind` est rejetée par `WsTransportSettings::validate()`. Un endpoint construit sans déclaration reste valide pour compatibilité, mais `subscription_capabilities()` retourne `None` et `supports_subscription(...)` retourne `false` : l'absence de déclaration n'est jamais une preuve implicite de support.
|
||||
|
||||
Deux appels `WsSession::connect` avec le même endpoint créent volontairement deux connexions physiques distinctes. Il n'existe encore aucun pool de sessions automatique.
|
||||
|
||||
Le socket brut et la primitive JSON-RPC générique ne sont pas publics. Le moteur générique de subscription typed reste `pub(crate)` ; il ne constitue donc pas une escape hatch provider-specific.
|
||||
@@ -387,9 +396,11 @@ let snapshot = stream.snapshot();
|
||||
let closed = stream.close().await;
|
||||
```
|
||||
|
||||
`try_update()` remplace dynamiquement la requête complète tant que la session est `Active`. Une mutation pendant `Reconnecting` est refusée pour éviter une application ambiguë. Le snapshot expose reconnects, replay attempts, gaps, duplicates, dernier `from_slot` demandé et dernier slot observé, sans endpoint ni payload arbitraire.
|
||||
`try_update()` remplace dynamiquement la requête complète tant que la session est `Active`. Une mutation pendant `Reconnecting` est refusée pour éviter une application ambiguë. Le snapshot expose reconnects, replay attempts, replay deliveries conservatrices, reprises dont la couverture reste non prouvée, gaps de rétention, duplicates, dernier `from_slot` demandé et dernier slot observé, sans endpoint ni payload arbitraire.
|
||||
|
||||
Le reconnect réutilise la dernière requête acceptée et peut avancer `from_slot`, mais le consumer doit traiter cette reprise comme best-effort. KSP ne promet ni exactly-once, ni replay historique complet, ni absence de fork/equivocation entre nœuds.
|
||||
Une `replay_delivery_count` n'augmente que lorsque le premier flux post-reconnect redélivre exactement le slot de reprise demandé. Elle prouve une livraison au bord de replay, pas la complétude de l'intervalle. Dès que le premier update slot-bearing atteint ou dépasse cette borne, `replay_coverage_unproven_count` augmente aussi : Transport ne possède pas de preuve générique que tous les updates correspondant aux filtres ont été livrés entre la borne et la reprise live. Le compteur augmente également si un nouveau reconnect survient avant tout matériau slot-bearing. Cette valeur signifie explicitement « couverture non prouvée » ; elle ne prouve pas qu'un événement filtré existait ou a été perdu.
|
||||
|
||||
Le reconnect réutilise la dernière requête acceptée et peut avancer `from_slot`, mais le consumer doit traiter cette reprise comme best-effort. `replay_attempt_count`, `replay_delivery_count` et une preuve de coverage sont des notions distinctes. Transport ne publie actuellement aucun compteur `replay_covered` générique, car ni l'acceptation de `from_slot` ni une livraison ponctuelle ne démontrent à elles seules une couverture historique complète. KSP ne promet ni exactly-once, ni replay historique complet, ni absence de fork/equivocation entre nœuds.
|
||||
|
||||
## 5. Appels typés
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/grpc_stream.rs
|
||||
// version: 4
|
||||
// version: 7
|
||||
|
||||
use tonic_prost::prost::Message; // rust-rules: trait-import
|
||||
|
||||
@@ -34,6 +34,8 @@ pub struct YellowstoneGrpcSubscribeSnapshot {
|
||||
continuity_gap_count: u64,
|
||||
duplicate_update_count: u64,
|
||||
replay_attempt_count: u64,
|
||||
replay_delivery_count: u64,
|
||||
replay_coverage_unproven_count: u64,
|
||||
last_requested_from_slot: std::option::Option<u64>,
|
||||
last_observed_slot: std::option::Option<u64>,
|
||||
terminal_error_code: std::option::Option<ksp_core_lib::ErrorCode>,
|
||||
@@ -84,6 +86,8 @@ impl YellowstoneGrpcSubscribeSnapshot {
|
||||
continuity_gap_count: 0,
|
||||
duplicate_update_count: 0,
|
||||
replay_attempt_count: 0,
|
||||
replay_delivery_count: 0,
|
||||
replay_coverage_unproven_count: 0,
|
||||
last_requested_from_slot: initial_from_slot,
|
||||
last_observed_slot: std::option::Option::None,
|
||||
terminal_error_code: std::option::Option::None,
|
||||
@@ -122,6 +126,25 @@ impl YellowstoneGrpcSubscribeSnapshot {
|
||||
return self.replay_attempt_count;
|
||||
}
|
||||
|
||||
/// Returns the number of replay-bearing reconnects that delivered the requested replay boundary slot again.
|
||||
///
|
||||
/// This is conservative delivery evidence only. It does not prove that every matching update in the replay interval was delivered and must never be
|
||||
/// interpreted as `replay_covered`.
|
||||
#[must_use]
|
||||
pub const fn replay_delivery_count(self) -> u64 {
|
||||
return self.replay_delivery_count;
|
||||
}
|
||||
|
||||
/// Returns the number of successful replay-bearing reconnects whose target coverage remains unproven.
|
||||
///
|
||||
/// The counter advances when the first post-reconnect slot-bearing update reaches or passes the requested replay boundary, because generic Transport
|
||||
/// cannot prove from that delivery alone that every matching update in the replay interval was delivered. It also advances when another reconnect starts
|
||||
/// before any slot-bearing replay material arrives. This is an explicit lack of coverage proof, not proof that a filtered event actually existed or was lost.
|
||||
#[must_use]
|
||||
pub const fn replay_coverage_unproven_count(self) -> u64 {
|
||||
return self.replay_coverage_unproven_count;
|
||||
}
|
||||
|
||||
/// Returns the most recent effective `from_slot` sent by KSP, including any clamp to `SubscribeReplayInfo.first_available`.
|
||||
#[must_use]
|
||||
pub const fn last_requested_from_slot(self) -> std::option::Option<u64> {
|
||||
@@ -273,8 +296,9 @@ impl SolanaYellowstoneGrpcSubscribeSession {
|
||||
|
||||
/// Receives the next decoded standard Yellowstone update.
|
||||
///
|
||||
/// Transient remote stream loss is hidden while KSP performs a bounded reconnect. Terminal remote statuses after the reconnect budget, malformed updates
|
||||
/// and overflow surface a safe KSP error. A normal close returns `Ok(None)`.
|
||||
/// Transient remote stream loss is hidden while KSP performs a bounded reconnect. A full bounded update queue applies asynchronous backpressure to the
|
||||
/// gRPC stream instead of dropping an update or terminating locally; terminal remote statuses after the reconnect budget and malformed updates still
|
||||
/// surface a safe KSP error. A normal close returns `Ok(None)`.
|
||||
pub async fn next_update(&mut self) -> ksp_core_lib::Result<std::option::Option<crate::YellowstoneSubscribeUpdate>> {
|
||||
return match self.update_rx.recv().await {
|
||||
std::option::Option::Some(std::result::Result::Ok(update)) => std::result::Result::Ok(std::option::Option::Some(update)),
|
||||
@@ -506,17 +530,44 @@ enum UpdateIdentity {
|
||||
}
|
||||
|
||||
struct ContinuityTracker {
|
||||
pending_replay_from_slot: std::option::Option<u64>,
|
||||
recent_order: std::collections::VecDeque<UpdateIdentity>,
|
||||
recent_set: std::collections::HashSet<UpdateIdentity>,
|
||||
}
|
||||
|
||||
impl ContinuityTracker {
|
||||
fn new() -> Self {
|
||||
return Self { recent_order: std::collections::VecDeque::new(), recent_set: std::collections::HashSet::new() };
|
||||
return Self {
|
||||
pending_replay_from_slot: std::option::Option::None,
|
||||
recent_order: std::collections::VecDeque::new(),
|
||||
recent_set: std::collections::HashSet::new(),
|
||||
};
|
||||
}
|
||||
|
||||
fn begin_replay(&mut self, from_slot: std::option::Option<u64>) {
|
||||
self.pending_replay_from_slot = from_slot;
|
||||
return;
|
||||
}
|
||||
|
||||
fn abandon_pending_replay(&mut self, snapshot: &mut crate::YellowstoneGrpcSubscribeSnapshot) -> bool {
|
||||
if self.pending_replay_from_slot.take().is_none() {
|
||||
return false;
|
||||
}
|
||||
snapshot.replay_coverage_unproven_count = snapshot.replay_coverage_unproven_count.saturating_add(1);
|
||||
return true;
|
||||
}
|
||||
|
||||
fn observe(&mut self, update: &crate::YellowstoneSubscribeUpdate, snapshot: &mut crate::YellowstoneGrpcSubscribeSnapshot) {
|
||||
if let std::option::Option::Some(slot) = update_slot(update) {
|
||||
if let std::option::Option::Some(requested) = self.pending_replay_from_slot
|
||||
&& slot >= requested
|
||||
{
|
||||
if slot == requested {
|
||||
snapshot.replay_delivery_count = snapshot.replay_delivery_count.saturating_add(1);
|
||||
}
|
||||
snapshot.replay_coverage_unproven_count = snapshot.replay_coverage_unproven_count.saturating_add(1);
|
||||
self.pending_replay_from_slot = std::option::Option::None;
|
||||
}
|
||||
snapshot.last_observed_slot = std::option::Option::Some(match snapshot.last_observed_slot {
|
||||
std::option::Option::Some(previous) => std::cmp::max(previous, slot),
|
||||
std::option::Option::None => slot,
|
||||
@@ -585,23 +636,30 @@ async fn run_subscribe_actor(
|
||||
}
|
||||
tracker.observe(&update, &mut snapshot);
|
||||
snapshot_tx.send_replace(snapshot);
|
||||
match update_tx.try_send(std::result::Result::Ok(update)) {
|
||||
std::result::Result::Ok(()) => {},
|
||||
std::result::Result::Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
endpoint_name = endpoint_name.as_str(),
|
||||
provider = provider.as_str(),
|
||||
cluster = cluster.as_str(),
|
||||
"Yellowstone subscribe update queue overflowed"
|
||||
);
|
||||
fail_actor(crate::ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW, &request_state, &mut snapshot, &snapshot_tx);
|
||||
let delivery = tokio::select! {
|
||||
biased;
|
||||
shutdown_changed = shutdown_rx.changed() => {
|
||||
let deadline = shutdown_deadline(&shutdown_rx, shutdown_changed);
|
||||
snapshot.state = crate::YellowstoneGrpcSubscribeState::Closing;
|
||||
snapshot_tx.send_replace(snapshot);
|
||||
clear_request_sender(&request_state);
|
||||
finish_client_half_close(
|
||||
&endpoint_name,
|
||||
&provider,
|
||||
&cluster,
|
||||
&mut incoming,
|
||||
deadline,
|
||||
&mut snapshot,
|
||||
&snapshot_tx,
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
},
|
||||
std::result::Result::Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
|
||||
}
|
||||
result = update_tx.send(std::result::Result::Ok(update)) => result,
|
||||
};
|
||||
if delivery.is_err() {
|
||||
close_actor(&request_state, &mut snapshot, &snapshot_tx);
|
||||
return;
|
||||
},
|
||||
}
|
||||
},
|
||||
std::result::Result::Ok(std::option::Option::None) => {
|
||||
@@ -620,6 +678,7 @@ async fn run_subscribe_actor(
|
||||
&mut shutdown_rx,
|
||||
&mut snapshot,
|
||||
&snapshot_tx,
|
||||
&mut tracker,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -662,6 +721,7 @@ async fn run_subscribe_actor(
|
||||
&mut shutdown_rx,
|
||||
&mut snapshot,
|
||||
&snapshot_tx,
|
||||
&mut tracker,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -701,8 +761,12 @@ async fn reconnect_subscribe_stream(
|
||||
shutdown_rx: &mut tokio::sync::watch::Receiver<std::option::Option<tokio::time::Instant>>,
|
||||
snapshot: &mut crate::YellowstoneGrpcSubscribeSnapshot,
|
||||
snapshot_tx: &tokio::sync::watch::Sender<crate::YellowstoneGrpcSubscribeSnapshot>,
|
||||
tracker: &mut ContinuityTracker,
|
||||
) -> ReconnectOutcome {
|
||||
clear_request_sender(request_state);
|
||||
if tracker.abandon_pending_replay(snapshot) {
|
||||
snapshot_tx.send_replace(*snapshot);
|
||||
}
|
||||
snapshot.state = crate::YellowstoneGrpcSubscribeState::Reconnecting;
|
||||
snapshot.terminal_error_code = std::option::Option::None;
|
||||
snapshot_tx.send_replace(*snapshot);
|
||||
@@ -785,6 +849,7 @@ async fn reconnect_subscribe_stream(
|
||||
return ReconnectOutcome::Exhausted(error);
|
||||
}
|
||||
snapshot.reconnect_count = snapshot.reconnect_count.saturating_add(1);
|
||||
tracker.begin_replay(effective_from_slot);
|
||||
snapshot.state = crate::YellowstoneGrpcSubscribeState::Active;
|
||||
snapshot.terminal_error_code = std::option::Option::None;
|
||||
snapshot_tx.send_replace(*snapshot);
|
||||
@@ -947,16 +1012,16 @@ async fn finish_client_half_close(
|
||||
},
|
||||
std::result::Result::Ok(std::result::Result::Err(status)) => {
|
||||
let code = status.code().to_string();
|
||||
ksp_logging_lib::warn!(
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
endpoint_name,
|
||||
provider = provider.as_str(),
|
||||
cluster = cluster.as_str(),
|
||||
grpc_code = code.as_str(),
|
||||
"Yellowstone subscribe endpoint returned a status during graceful shutdown"
|
||||
"Yellowstone subscribe endpoint returned a status after local graceful shutdown request; treating session as closed"
|
||||
);
|
||||
snapshot.state = crate::YellowstoneGrpcSubscribeState::Failed;
|
||||
snapshot.terminal_error_code = std::option::Option::Some(crate::ERROR_CODE_GRPC_STATUS);
|
||||
snapshot.state = crate::YellowstoneGrpcSubscribeState::Closed;
|
||||
snapshot.terminal_error_code = std::option::Option::None;
|
||||
snapshot_tx.send_replace(*snapshot);
|
||||
return;
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
||||
// version: 47
|
||||
// version: 48
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -555,6 +555,8 @@ pub use self::ws_protocol_session::HeliusLaserStreamWsSession;
|
||||
pub use self::ws_protocol_session::SolanaStandardWsSession;
|
||||
/// Shareable compatibility handle for one explicitly created standard Solana physical WebSocket session.
|
||||
pub use self::ws_session::WsSession;
|
||||
/// Cloneable latest-value observer for one safe physical WebSocket session snapshot.
|
||||
pub use self::ws_session::WsSessionSnapshotSource;
|
||||
/// Open cluster or network descriptor used by WebSocket endpoint settings.
|
||||
pub use self::ws_settings::WsClusterName;
|
||||
/// Runtime settings for one named WebSocket endpoint.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
/// Typed facade for one standard Solana WebSocket physical session.
|
||||
///
|
||||
@@ -42,6 +42,12 @@ impl SolanaStandardWsSession {
|
||||
return self.inner.snapshot();
|
||||
}
|
||||
|
||||
/// Returns a cloneable latest-value observer for safe physical-session snapshots from the shared actor.
|
||||
#[must_use]
|
||||
pub fn snapshot_source(&self) -> crate::WsSessionSnapshotSource {
|
||||
return self.inner.snapshot_source();
|
||||
}
|
||||
|
||||
/// Returns the latest observable physical-session state.
|
||||
#[must_use]
|
||||
pub fn state(&self) -> crate::WsSessionState {
|
||||
@@ -117,6 +123,12 @@ impl HeliusLaserStreamWsSession {
|
||||
return self.inner.snapshot();
|
||||
}
|
||||
|
||||
/// Returns a cloneable latest-value observer for safe physical-session snapshots from the shared actor.
|
||||
#[must_use]
|
||||
pub fn snapshot_source(&self) -> crate::WsSessionSnapshotSource {
|
||||
return self.inner.snapshot_source();
|
||||
}
|
||||
|
||||
/// Returns the latest observable physical-session state.
|
||||
#[must_use]
|
||||
pub fn state(&self) -> crate::WsSessionState {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/ws_session.rs
|
||||
// version: 14
|
||||
// version: 15
|
||||
|
||||
use futures_util::SinkExt; // rust-rules: trait-import
|
||||
use futures_util::StreamExt; // rust-rules: trait-import
|
||||
@@ -10,6 +10,43 @@ const HELIUS_WS_HEARTBEAT_INTERVAL: std::time::Duration = std::time::Duration::f
|
||||
|
||||
type WsPhysicalStream = tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>;
|
||||
|
||||
/// Cloneable latest-value observer for one physical WebSocket session snapshot.
|
||||
///
|
||||
/// The observer exposes only the already-safe [`crate::WsSessionSnapshot`] projection and keeps the internal Tokio watch channel private. Cloning it does
|
||||
/// not create another socket, actor, reconnect loop or subscription registry.
|
||||
#[derive(Clone)]
|
||||
pub struct WsSessionSnapshotSource {
|
||||
receiver: tokio::sync::watch::Receiver<crate::WsSessionSnapshot>,
|
||||
}
|
||||
|
||||
impl crate::WsSessionSnapshotSource {
|
||||
fn new(receiver: tokio::sync::watch::Receiver<crate::WsSessionSnapshot>) -> Self {
|
||||
return Self { receiver };
|
||||
}
|
||||
|
||||
/// Returns the current safe physical-session snapshot without waiting for another actor transition.
|
||||
#[must_use]
|
||||
pub fn current(&self) -> crate::WsSessionSnapshot {
|
||||
return (*self.receiver.borrow()).clone();
|
||||
}
|
||||
|
||||
/// Waits for one newer safe physical-session snapshot.
|
||||
///
|
||||
/// `None` means the owning WebSocket actor dropped the latest-value publisher and no further snapshot can arrive.
|
||||
pub async fn wait_for_change(&mut self) -> std::option::Option<crate::WsSessionSnapshot> {
|
||||
return match self.receiver.changed().await {
|
||||
std::result::Result::Ok(()) => std::option::Option::Some((*self.receiver.borrow_and_update()).clone()),
|
||||
std::result::Result::Err(_) => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for crate::WsSessionSnapshotSource {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter.debug_struct("WsSessionSnapshotSource").field("current", &self.current()).finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// Shareable handle for one explicitly created physical WebSocket session.
|
||||
///
|
||||
/// The handle never exposes the sensitive endpoint URL or the underlying socket. All socket I/O is owned by one internal actor task and all caller
|
||||
@@ -124,6 +161,12 @@ impl WsSession {
|
||||
return self.snapshot_rx.borrow().clone();
|
||||
}
|
||||
|
||||
/// Returns a cloneable latest-value observer for safe physical-session snapshots.
|
||||
#[must_use]
|
||||
pub fn snapshot_source(&self) -> crate::WsSessionSnapshotSource {
|
||||
return crate::WsSessionSnapshotSource::new(self.snapshot_rx.clone());
|
||||
}
|
||||
|
||||
/// Returns the latest observable physical-session state.
|
||||
#[must_use]
|
||||
pub fn state(&self) -> crate::WsSessionState {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/ws_settings.rs
|
||||
// version: 4
|
||||
// version: 6
|
||||
|
||||
const DEFAULT_WS_CLOSE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
const DEFAULT_WS_COMMAND_QUEUE_CAPACITY: usize = 128;
|
||||
@@ -401,6 +401,7 @@ pub struct WsEndpointSettings {
|
||||
provider: crate::WsProviderName,
|
||||
cluster: crate::WsClusterName,
|
||||
protocol: crate::WsProtocolKind,
|
||||
subscription_capabilities: std::option::Option<std::vec::Vec<crate::WsSubscriptionKind>>,
|
||||
url: crate::WsEndpointUrl,
|
||||
session: crate::WsSessionSettings,
|
||||
}
|
||||
@@ -417,7 +418,42 @@ impl WsEndpointSettings {
|
||||
url: crate::WsEndpointUrl,
|
||||
session: crate::WsSessionSettings,
|
||||
) -> Self {
|
||||
return Self { name: name.into(), enabled, provider, cluster, protocol, url, session };
|
||||
return Self {
|
||||
name: name.into(),
|
||||
enabled,
|
||||
provider,
|
||||
cluster,
|
||||
protocol,
|
||||
subscription_capabilities: std::option::Option::None,
|
||||
url,
|
||||
session,
|
||||
};
|
||||
}
|
||||
|
||||
/// Declares the exact WebSocket subscription families supported by this endpoint.
|
||||
///
|
||||
/// An endpoint created without this builder remains valid for backward compatibility, but its capabilities are considered undeclared and therefore
|
||||
/// never constitute positive support evidence for a subscription family. Validation rejects an explicitly declared empty set, duplicates and
|
||||
/// capabilities that cannot belong to the selected protocol surface.
|
||||
#[must_use]
|
||||
pub fn with_subscription_capabilities(mut self, capabilities: std::vec::Vec<crate::WsSubscriptionKind>) -> Self {
|
||||
self.subscription_capabilities = std::option::Option::Some(capabilities);
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Returns the explicitly declared WebSocket subscription capabilities, or `None` when the endpoint still uses the legacy undeclared form.
|
||||
#[must_use]
|
||||
pub fn subscription_capabilities(&self) -> std::option::Option<&[crate::WsSubscriptionKind]> {
|
||||
return self.subscription_capabilities.as_deref();
|
||||
}
|
||||
|
||||
/// Returns whether this endpoint explicitly declares support for one WebSocket subscription family.
|
||||
///
|
||||
/// Undeclared legacy capability state is fail-closed and therefore returns `false`.
|
||||
#[must_use]
|
||||
pub fn supports_subscription(&self, kind: crate::WsSubscriptionKind) -> bool {
|
||||
return ws_protocol_supports_subscription(self.protocol, kind)
|
||||
&& self.subscription_capabilities().is_some_and(|capabilities| return capabilities.contains(&kind));
|
||||
}
|
||||
|
||||
/// Returns the logical endpoint name.
|
||||
@@ -532,6 +568,32 @@ fn validate_ws_endpoint(endpoint: &crate::WsEndpointSettings, endpoint_index: us
|
||||
if let std::result::Result::Err(error) = validate_ws_descriptor(endpoint.cluster().as_str(), cluster_field.as_str()) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if let std::option::Option::Some(capabilities) = endpoint.subscription_capabilities() {
|
||||
if capabilities.is_empty() {
|
||||
return ws_invalid_settings(
|
||||
"explicit WebSocket subscription capabilities must not be empty",
|
||||
format!("ws_endpoints[{endpoint_index}].subscription_capabilities").as_str(),
|
||||
);
|
||||
}
|
||||
for (capability_index, capability) in capabilities.iter().copied().enumerate() {
|
||||
let field = format!("ws_endpoints[{endpoint_index}].subscription_capabilities[{capability_index}]");
|
||||
if !ws_protocol_supports_subscription(endpoint.protocol(), capability) {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "WebSocket subscription capability is incompatible with endpoint protocol")
|
||||
.with_context("field", field)
|
||||
.with_context("subscription_kind", capability.as_str())
|
||||
.with_context("protocol", endpoint.protocol().as_str()),
|
||||
);
|
||||
}
|
||||
if capabilities[..capability_index].contains(&capability) {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "WebSocket subscription capabilities must be unique")
|
||||
.with_context("field", field)
|
||||
.with_context("subscription_kind", capability.as_str()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let std::result::Result::Err(error) = endpoint.session().validate() {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
@@ -547,6 +609,34 @@ fn validate_ws_endpoint(endpoint: &crate::WsEndpointSettings, endpoint_index: us
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
const fn ws_protocol_supports_subscription(protocol: crate::WsProtocolKind, kind: crate::WsSubscriptionKind) -> bool {
|
||||
return match protocol {
|
||||
crate::WsProtocolKind::SolanaStandard => matches!(
|
||||
kind,
|
||||
crate::WsSubscriptionKind::Account
|
||||
| crate::WsSubscriptionKind::Block
|
||||
| crate::WsSubscriptionKind::Logs
|
||||
| crate::WsSubscriptionKind::Program
|
||||
| crate::WsSubscriptionKind::Root
|
||||
| crate::WsSubscriptionKind::Signature
|
||||
| crate::WsSubscriptionKind::Slot
|
||||
| crate::WsSubscriptionKind::SlotsUpdates
|
||||
| crate::WsSubscriptionKind::Vote
|
||||
),
|
||||
crate::WsProtocolKind::HeliusLaserStream => matches!(
|
||||
kind,
|
||||
crate::WsSubscriptionKind::Account
|
||||
| crate::WsSubscriptionKind::Logs
|
||||
| crate::WsSubscriptionKind::Program
|
||||
| crate::WsSubscriptionKind::Root
|
||||
| crate::WsSubscriptionKind::Signature
|
||||
| crate::WsSubscriptionKind::Slot
|
||||
| crate::WsSubscriptionKind::SlotsUpdates
|
||||
| crate::WsSubscriptionKind::HeliusTransaction
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn validate_ws_descriptor(value: &str, field: &str) -> ksp_core_lib::Result<()> {
|
||||
if value.trim().is_empty() {
|
||||
return ws_invalid_settings("WebSocket transport descriptor must not be empty", field);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||
// version: 52
|
||||
// version: 55
|
||||
|
||||
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
||||
|
||||
@@ -569,6 +569,17 @@ fn public_v0_2_7_pre_004_physical_websocket_session_contract_is_available_from_c
|
||||
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_WS_SESSION_CLOSED.code(), "ws_session_closed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_v0_3_14_pre_003_websocket_snapshot_source_is_available_from_crate_root() {
|
||||
let _physical = ksp_onchain_transport_lib::WsSession::snapshot_source;
|
||||
let _standard = ksp_onchain_transport_lib::SolanaStandardWsSession::snapshot_source;
|
||||
let _helius = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::snapshot_source;
|
||||
let _current = ksp_onchain_transport_lib::WsSessionSnapshotSource::current;
|
||||
let _wait = ksp_onchain_transport_lib::WsSessionSnapshotSource::wait_for_change;
|
||||
assert!(std::any::type_name::<ksp_onchain_transport_lib::WsSessionSnapshotSource>().ends_with("WsSessionSnapshotSource"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_v0_2_7_pre_005_bounded_websocket_close_contract_is_available_from_crate_root() {
|
||||
let _close = ksp_onchain_transport_lib::WsSession::close;
|
||||
@@ -1021,6 +1032,8 @@ fn public_v0_2_9_pre_010_yellowstone_reconnect_snapshot_is_available_from_crate_
|
||||
let _gap_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::continuity_gap_count;
|
||||
let _duplicate_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::duplicate_update_count;
|
||||
let _replay_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::replay_attempt_count;
|
||||
let _delivery_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::replay_delivery_count;
|
||||
let _coverage_unproven_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::replay_coverage_unproven_count;
|
||||
let _requested = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::last_requested_from_slot;
|
||||
let _observed = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::last_observed_slot;
|
||||
let _terminal = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::terminal_error_code;
|
||||
@@ -1044,3 +1057,21 @@ fn public_v0_3_13_pre_002_yellowstone_subscribe_identity_is_opaque_and_available
|
||||
assert!(!debug.contains("bytes:"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_v0_3_15_pre_002_websocket_endpoint_capability_contract_is_available_from_crate_root() {
|
||||
let endpoint = ksp_onchain_transport_lib::WsEndpointSettings::new(
|
||||
"typed_public",
|
||||
true,
|
||||
ksp_onchain_transport_lib::WsProviderName::new("public"),
|
||||
ksp_onchain_transport_lib::WsClusterName::new("devnet"),
|
||||
ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard,
|
||||
ksp_onchain_transport_lib::WsEndpointUrl::parse("wss://api.devnet.solana.com").expect("public WebSocket URL fixture must parse"),
|
||||
ksp_onchain_transport_lib::WsSessionSettings::default(),
|
||||
)
|
||||
.with_subscription_capabilities(std::vec![ksp_onchain_transport_lib::WsSubscriptionKind::Logs, ksp_onchain_transport_lib::WsSubscriptionKind::Block,]);
|
||||
assert!(endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Logs));
|
||||
assert!(endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::Block));
|
||||
assert!(!endpoint.supports_subscription(ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction));
|
||||
assert_eq!(endpoint.subscription_capabilities().map(|capabilities| return capabilities.len()), std::option::Option::Some(2));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||
// version: 44
|
||||
// version: 49
|
||||
|
||||
//! Release-level completeness canaries for staged HTTP and WebSocket Transport coverage.
|
||||
|
||||
@@ -1354,3 +1354,105 @@ fn release_v0_3_13_pre_002_yellowstone_subscribe_identity_remains_opaque_and_dep
|
||||
assert!(!root.contains("pub use yellowstone_grpc_proto"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_v0_3_14_pre_003_ws_snapshot_source_reuses_actor_watch_without_second_runtime() {
|
||||
let session_source = include_str!("../src/ws_session.rs");
|
||||
let facade_source = include_str!("../src/ws_protocol_session.rs");
|
||||
let session_tests = include_str!("../unit_tests/ws_session.rs");
|
||||
assert!(session_source.contains("pub struct WsSessionSnapshotSource"));
|
||||
assert!(session_source.contains("self.snapshot_rx.clone()"));
|
||||
assert!(facade_source.matches("pub fn snapshot_source").count() >= 2);
|
||||
for required in [
|
||||
"v0_3_14_pre_003_snapshot_source_tracks_latest_value_without_owning_runtime",
|
||||
"websocket_reconnect_resubscribes_in_local_id_order_and_remaps_remote_ids",
|
||||
"websocket_notification_queue_overflow_fails_only_slow_subscription_and_cleans_remote_binding",
|
||||
] {
|
||||
assert!(session_tests.contains(required), "missing pre.003 WebSocket continuity test: {required}");
|
||||
}
|
||||
assert!(!session_source.contains("WsSessionSnapshotSource {\n socket:"));
|
||||
assert!(!facade_source.contains("tokio_tungstenite::connect_async"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_v0_3_14_pre_004_native_replay_evidence_never_conflates_attempt_delivery_and_coverage() {
|
||||
let stream_source = include_str!("../src/grpc_stream.rs");
|
||||
let stream_tests = include_str!("../unit_tests/grpc_stream.rs");
|
||||
for required in [
|
||||
"replay_attempt_count",
|
||||
"replay_delivery_count",
|
||||
"replay_coverage_unproven_count",
|
||||
"pending_replay_from_slot",
|
||||
"begin_replay",
|
||||
"abandon_pending_replay",
|
||||
"first_available > requested",
|
||||
"This is conservative delivery evidence only",
|
||||
"explicit lack of coverage proof",
|
||||
] {
|
||||
assert!(stream_source.contains(required), "missing pre.004 native replay evidence token: {required}");
|
||||
}
|
||||
assert!(stream_tests.contains("v0_3_14_pre_004_replay_acceptance_without_boundary_delivery_never_claims_coverage"));
|
||||
assert!(stream_tests.contains("yellowstone_reconnect_replays_from_last_observed_slot_and_counts_duplicate_identity"));
|
||||
assert!(stream_tests.contains("yellowstone_replay_info_proves_and_clamps_retention_gap_without_lossless_claim"));
|
||||
assert!(!stream_source.contains("replay_covered_count"));
|
||||
assert!(!stream_source.contains("replay_coverage_proven_count"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_v0_3_15_pre_002_websocket_capabilities_are_endpoint_declared_typed_and_protocol_bounded() {
|
||||
let settings_source = include_str!("../src/ws_settings.rs");
|
||||
assert!(settings_source.contains("subscription_capabilities: std::option::Option<std::vec::Vec<crate::WsSubscriptionKind>>"));
|
||||
assert!(settings_source.contains("pub fn with_subscription_capabilities"));
|
||||
assert!(settings_source.contains("pub fn subscription_capabilities"));
|
||||
assert!(settings_source.contains("pub fn supports_subscription"));
|
||||
assert!(settings_source.contains("crate::WsProtocolKind::SolanaStandard => matches!("));
|
||||
assert!(settings_source.contains("crate::WsProtocolKind::HeliusLaserStream => matches!("));
|
||||
assert!(!settings_source.contains("provider().as_str() == \"helius\""));
|
||||
assert!(!settings_source.contains("provider().as_str() == \"publicnode\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_v0_3_15_pre_015_yellowstone_update_delivery_backpressures_without_local_overflow() {
|
||||
let stream_source = include_str!("../src/grpc_stream.rs");
|
||||
let stream_tests = include_str!("../unit_tests/grpc_stream.rs");
|
||||
let actor_start = stream_source.find("async fn run_subscribe_actor(").expect("Subscribe actor must remain present");
|
||||
let actor_tail = &stream_source[actor_start..];
|
||||
let actor_end = actor_tail
|
||||
.find(
|
||||
"
|
||||
enum ReconnectOutcome",
|
||||
)
|
||||
.expect("Subscribe actor boundary must remain present");
|
||||
let actor = &actor_tail[..actor_end];
|
||||
assert!(actor.contains("result = update_tx.send(std::result::Result::Ok(update)) => result"));
|
||||
assert!(actor.contains("shutdown_changed = shutdown_rx.changed()"));
|
||||
assert!(actor.contains("biased;"));
|
||||
assert!(!actor.contains("Yellowstone subscribe update queue overflowed"));
|
||||
assert!(stream_source.contains("ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW"));
|
||||
assert!(!stream_source.contains("unbounded_channel"));
|
||||
assert!(stream_tests.contains("yellowstone_slow_receiver_applies_bounded_backpressure_without_terminal_overflow"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_v0_3_15_pre_015_fix_001_local_close_status_is_closed_without_weakening_active_status_failure() {
|
||||
let stream_source = include_str!("../src/grpc_stream.rs");
|
||||
let stream_tests = include_str!("../unit_tests/grpc_stream.rs");
|
||||
let close_start = stream_source.find("async fn finish_client_half_close(").expect("graceful half-close helper must remain present");
|
||||
let close_tail = &stream_source[close_start..];
|
||||
let close_end = close_tail.find("\nfn stream_status_error(").expect("graceful half-close helper boundary must remain present");
|
||||
let close_helper = &close_tail[..close_end];
|
||||
let actor_start = stream_source.find("async fn run_subscribe_actor(").expect("Subscribe actor must remain present");
|
||||
let actor_tail = &stream_source[actor_start..];
|
||||
let actor_end = actor_tail.find("\nenum ReconnectOutcome").expect("Subscribe actor boundary must remain present");
|
||||
let actor = &actor_tail[..actor_end];
|
||||
assert!(close_helper.contains("returned a status after local graceful shutdown request; treating session as closed"));
|
||||
assert!(close_helper.contains("snapshot.state = crate::YellowstoneGrpcSubscribeState::Closed"));
|
||||
assert!(close_helper.contains("snapshot.terminal_error_code = std::option::Option::None"));
|
||||
assert!(!close_helper.contains("snapshot.terminal_error_code = std::option::Option::Some(crate::ERROR_CODE_GRPC_STATUS)"));
|
||||
assert!(actor.contains("stream_status_error(\"Subscribe\", status"));
|
||||
assert!(actor.contains("fail_actor(crate::ERROR_CODE_GRPC_STATUS"));
|
||||
assert!(stream_tests.contains("yellowstone_explicit_close_accepts_remote_status_after_local_half_close"));
|
||||
assert!(stream_tests.contains("yellowstone_stream_remote_status_is_safe_and_terminal"));
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user