Compare commits
180 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 | |||
| 9785cda39d | |||
| 0cea99cd5e | |||
| 71bc754feb | |||
| cabbfed730 | |||
| 8b6ef7778a | |||
| 06319655b0 | |||
| f636169783 | |||
| 0f8f38ff52 | |||
| 142998afb3 | |||
| 665d7d2bb6 | |||
| 1746d47412 | |||
| 9bb2f7bcae | |||
| 1f2720463b | |||
| 8bb530e9b4 | |||
| 72df965a9c | |||
| f688325170 | |||
| c711213dcc | |||
| 11ecdccd65 | |||
| 27ab84d198 | |||
| 28e4da3879 | |||
| 31c0a83e51 | |||
| 34fddc3bf5 | |||
| 13c3dac554 | |||
| f7ed053e8e | |||
| 2e8feb56dd | |||
| 793178b345 | |||
| ee0359efd5 | |||
| 1dc57a85c2 | |||
| e7e8960fec | |||
| 79e278064d | |||
| 474b894d1c | |||
| 0c76b74afe | |||
| b608ed2c50 | |||
| f9d4e6bc74 | |||
| 223f11b989 | |||
| 5095be0b0d | |||
| 2aa64cf0a4 | |||
| 90c266d68a | |||
| 8410b2f4ef | |||
| ba69a5ad20 | |||
| 07b9b20eb5 | |||
| 901a0515b7 | |||
| 50c01a19c9 | |||
| 3a8fa87acb | |||
| df1dbbb8fb | |||
| 531ca851c4 | |||
| b3fd74529d | |||
| 00b376d234 | |||
| fa720d0054 | |||
| 91438e8214 | |||
| 08e9a34183 | |||
| 85937b8cbb | |||
| 107986bf85 | |||
| a8cabbebd5 | |||
| 636a0c5f43 | |||
| bf46782db3 | |||
| 02919e959e | |||
| d231eba8be | |||
| 637382f364 | |||
| bf6ef5b426 | |||
| 822e8fcd86 | |||
| 63e1a866a3 | |||
| 22c88c449d | |||
| ed6c0ac10c | |||
| 355c5d1e9a | |||
| 61d8b1dee2 | |||
| 0db32a865c | |||
| a4061ee1e1 | |||
| ec64be340e | |||
| f1c383c392 | |||
| 984c327162 | |||
| e2689d1b04 | |||
| b5c84f3df8 | |||
| 36c683df9e | |||
| 510adcb49b | |||
| ab87fd23cd | |||
| 1a352aa8d0 | |||
| 919b4ed46d | |||
| f7c57f21c5 | |||
| d4f4237723 | |||
| ba574f3052 | |||
| 0683b70abe | |||
| eaf6fae94c | |||
| e19a202565 | |||
| 8309d829cc | |||
| aabe8dedb2 | |||
| d0da907c00 | |||
| 6b434750c5 | |||
| c9047af626 | |||
| 52d692e8ec | |||
| 5c3c8b3fef | |||
| b0079fc3ee | |||
| ecfa123a79 | |||
| 9da372e4a2 | |||
| 9a998e7b52 | |||
| c3c9a80310 | |||
| 116eabba7d | |||
| e6c2649410 | |||
| e85d6f6963 | |||
| eb39aaa904 | |||
| bb7ee3cc3a | |||
| 84647c12ea | |||
| 156d237460 | |||
| 7d0710a1e6 | |||
| 5c797827f7 | |||
| b87b7f4505 | |||
| 92f08ca5a8 |
10
.env.example
10
.env.example
@@ -1,5 +1,5 @@
|
|||||||
# file: .env.example
|
# file: .env.example
|
||||||
# version: 13
|
# version: 14
|
||||||
|
|
||||||
# KSP Logging root directory. Used by config/std.logging.json for relative log output paths.
|
# KSP Logging root directory. Used by config/std.logging.json for relative log output paths.
|
||||||
# The current Config document fallback is "logs" when neither the process environment nor .env defines this variable.
|
# The current Config document fallback is "logs" when neither the process environment nor .env defines this variable.
|
||||||
@@ -28,16 +28,16 @@ KSP_WALLETS_DIRECTORY=wallets
|
|||||||
KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL=https://api.devnet.solana.com
|
KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL=https://api.devnet.solana.com
|
||||||
|
|
||||||
# Optional public Solana Mainnet HTTP endpoint override used by config/std.transport.json and its example.
|
# Optional public Solana Mainnet HTTP endpoint override used by config/std.transport.json and its example.
|
||||||
# The committed Transport document falls back to https://api.mainnet-beta.solana.com when this variable is absent.
|
# The committed Transport document falls back to https://api.mainnet.solana.com when this variable is absent.
|
||||||
KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL=https://api.mainnet-beta.solana.com
|
KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL=https://api.mainnet.solana.com
|
||||||
|
|
||||||
# Optional public Solana Devnet WebSocket endpoint override used by config/std.transport.json.
|
# Optional public Solana Devnet WebSocket endpoint override used by config/std.transport.json.
|
||||||
# The committed Transport document falls back to wss://api.devnet.solana.com when this variable is absent.
|
# The committed Transport document falls back to wss://api.devnet.solana.com when this variable is absent.
|
||||||
KSP_PUBLIC_SOLANA_DEVNET_WS_URL=wss://api.devnet.solana.com
|
KSP_PUBLIC_SOLANA_DEVNET_WS_URL=wss://api.devnet.solana.com
|
||||||
|
|
||||||
# Optional public Solana Mainnet WebSocket endpoint override used by config/std.transport.json and its example.
|
# Optional public Solana Mainnet WebSocket endpoint override used by config/std.transport.json and its example.
|
||||||
# The committed Transport document falls back to wss://api.mainnet-beta.solana.com when this variable is absent.
|
# The committed Transport document falls back to wss://api.mainnet.solana.com when this variable is absent.
|
||||||
KSP_PUBLIC_SOLANA_MAINNET_WS_URL=wss://api.mainnet-beta.solana.com
|
KSP_PUBLIC_SOLANA_MAINNET_WS_URL=wss://api.mainnet.solana.com
|
||||||
|
|
||||||
# Optional complete private-provider HTTP endpoint URL used only by the Transport example when explicitly selected.
|
# Optional complete private-provider HTTP endpoint URL used only by the Transport example when explicitly selected.
|
||||||
# Keep provider credentials in a KSP_SECRET_* variable; do not copy a real credential-bearing URL into committed JSON.
|
# Keep provider credentials in a KSP_SECRET_* variable; do not copy a real credential-bearing URL into committed JSON.
|
||||||
|
|||||||
92
CHANGELOG.md
92
CHANGELOG.md
@@ -1,8 +1,98 @@
|
|||||||
<!-- file: CHANGELOG.md -->
|
<!-- file: CHANGELOG.md -->
|
||||||
<!-- version: 28 -->
|
<!-- version: 35 -->
|
||||||
|
|
||||||
# Changelog KSP
|
# 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.
|
||||||
|
|
||||||
|
La convergence partage un registre global d'hydration entre Yellowstone, Standard Logs et Helius afin de coalescer `(network, signature, commitment)` avant fan-out HTTP. Après canonicalisation, les acquisitions de même `(network, signature)` sont sérialisées de manière bornée : un contenu identique conserve les observations/provenances distinctes, tandis qu'une divergence devient un `content_conflict` terminal explicite. Aucune majorité provider, préférence silencieuse ou stratégie `first-provider-wins` n'est introduite. Standard Block et HTTP Block Polling admettent directement les transactions Legacy/V0/V1 qualifiées avec `maxSupportedTransactionVersion = 1`.
|
||||||
|
|
||||||
|
Le supervisor multi-source possède toutes les tâches et applique des bornes source-neutral sur admission, pending signals, hydrations et persistence avec une fairness minimale entre sources reference-bearing. Les snapshots agrègent activity/health/backpressure/reconnect/replay/gap sans exposer le matériau provider. La politique reste conservative : une source configurée qui échoue est terminale faute d'équivalence de coverage prouvée ; le gap repair, les rôles degraded/failover et la preuve de couverture restent donc `0.3.14`. Le shutdown stop/fault/drain/abort+join est durci contre les races, leaders d'hydration abandonnés, Store lent et publications tardives.
|
||||||
|
|
||||||
|
Le gate déterministe final passe `cargo 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 850` 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 `transactionSubscribe` live, `blockSubscribe` provider dédié, HTTP polling Worker end-to-end et Worker multi-source vers Store restent explicitement non exécutés faute de gate provisionné ; aucune fixture ne les requalifie en PASS.
|
||||||
|
|
||||||
|
`prompts/033-V0_3_14_START_PROMPT.md` ouvre `0.3.14` exclusivement depuis le futur tag stable `v0.3.13`. Cette release doit fermer le gap repair/hardening multi-source limité au run actif — replay adressable, coverage redondante, scan HTTP borné, hydration de réparation, unresolved gaps et health policy — sans transformer le Worker en moteur historique ni créer de dépendance vers `ksp-job-backfill-lib`.
|
||||||
|
|
||||||
|
## 0.3.12 — Yellowstone + hydration HTTP + continuité de run du Worker RawTransaction — 2026-09-09
|
||||||
|
|
||||||
|
`0.3.12` ouvre la première source réseau productive de `ksp-worker-raw-transaction-ingest-lib` sans remettre en cause la fondation source-neutral de `0.3.11`. Le Worker dépend désormais de la façade `ksp-onchain-transport-lib` mais reste sans Config, Job Backfill, backend Store physique, `reqwest`, `tonic` ou proto provider direct. `RawTransactionIngestRuntimeResources` reçoit une source Yellowstone caller-composed et un pool HTTP cohérent ; `start` conserve la fondation sans source, tandis que `start_with_runtime_resources` lance la verticale live supervisée.
|
||||||
|
|
||||||
|
Les updates Yellowstone `Transaction`, `TransactionStatus` et les transactions incluses dans `Block` sont projetées vers des signaux source-neutral, coalescées de manière bornée par `(network, signature, commitment)`, hydratées via `getTransaction` observed puis converties par `ksp-raw-transaction-lib` avant admission/persistence Store. `BlockMeta` et `Slot` restent continuity-only. La provenance composite conserve les identités logiques sûres de la source et de la route d’hydration, sans URL, secret ni payload distant. Les gardes réseau/signature/slot/index/commitment empêchent les croisements de matériau avant admission.
|
||||||
|
|
||||||
|
La release matérialise une processing frontier strictement run-local : le Worker distingue travail pending/settled, n’avance jamais au travers d’un pending plus ancien et compacte ses états sans revendiquer une preuve de complétude durable. Le reconnect/replay reste Transport-owned : `from_slot` et `SubscribeReplayInfo` ne sont pas pilotés par le Worker. Le Worker projette un état source-neutral `Active/Reconnecting/Closing/Closed/Failed`, compte reconnect/replay-attempt/continuity-gap et fault avec `source_failed` lorsqu’une hausse du gap Transport prouve que la rétention ne permet plus la reprise demandée. Aucun Backfill ou repair automatique n’est déclenché.
|
||||||
|
|
||||||
|
Le hardening final ferme les duplicate storms, saturation/coalescence, Store lent/fautif, stop/fault pendant hydration/reconnect, overflow de compteurs, abandon des pending sans faux `settled`, abort/join des tâches privées et absence de retry HTTP possédé par le Worker. `pre.010` ajoute les canaris cross-layer Transport -> Worker -> Common RAW -> Store et verrouille les dependency firewalls, fixtures Legacy/V0, redaction et inventaires publics.
|
||||||
|
|
||||||
|
Le gate déterministe `pre.011` passe rustfmt check, audits Rust/Markdown, `cargo check --workspace`, Clippy strict, `cargo test --workspace --all-targets --all-features`, les suites ciblées et les trois inspections Cargo demandées. Les smokes live keyless HTTP Devnet et WebSocket Devnet sont exécutés séparément et passent ; les smokes Yellowstone token-gated et le Worker end-to-end Yellowstone + HTTP + Store restent explicitement non exécutés faute de ressources fournies, sans faux PASS. `pre.012` réconcilie ensuite README/USAGE et les architectures concernées avec cette verticale réellement matérialisée.
|
||||||
|
|
||||||
|
La suite reste découpée : `0.3.13` ajoute WS standard, Helius `transactionSubscribe`, HTTP live polling et la convergence multi-source ; `0.3.14` ferme le gap repair/hardening multi-source. `prompts/032-V0_3_13_START_PROMPT.md` ouvre `0.3.13` uniquement depuis le tag stable `v0.3.12` et impose un `pre.001` d’audit/sizing avant toute extension productive.
|
||||||
|
|
||||||
|
## 0.3.11 — Fondation runtime source-neutral du Worker RawTransaction ingest — 2026-09-08
|
||||||
|
|
||||||
|
`0.3.11` introduit `ksp-worker-raw-transaction-ingest-lib` comme premier Worker concret KSP pour l’acquisition continue de `RawTransaction`, tout en fermant volontairement la release **sans source réseau productive**. La crate consomme `ksp-worker-api`, `ksp-raw-transaction-lib` et la seule façade `ksp-store-lib`; elle ne dépend ni de Config, ni d’un Job, ni d’un backend Store physique, ni encore de `ksp-onchain-transport-lib`. Son identité stable est `raw_transaction_ingest`, ses settings bornent la capacité d’admission, la concurrence de persistence et la deadline de drain, et son `start` utilise exclusivement le runtime Tokio actif fourni par le caller.
|
||||||
|
|
||||||
|
Le runtime possède un supervisor privé, des tâches source/persistence détenues, une admission centrale `mpsc` bornée avec backpressure, la canonicalisation/assembly Common RAW, une observation key déterministe et la persistence atomique en mode `Normal`. Les outcomes distinguent insert, idempotence et tombstone purgée ; un contenu divergent devient `worker_raw_transaction_ingest.content_conflict`, les erreurs Store restent classifiées sans texte distant, et aucun provider gagnant n’est choisi implicitement. Les snapshots concrets latest-value se projettent directement sur `WorkerSnapshotSource`, utilisent séquences/compteurs checked et conservent le terminal pour les lecteurs tardifs.
|
||||||
|
|
||||||
|
Le hardening final ferme les races stop/fault, les source failures, la saturation observable, le Store lent/fautif et le shutdown borné. À l’expiration de `shutdown_drain_timeout`, le Worker ferme les admissions, abort les tâches restantes, les rejoint toutes puis publie `drain_timeout`; aucune tâche privée ne survit au terminal. Les tests externes de hardening/release verrouillent les sept codes d’erreur publics, les huit modules de production, les vingt-quatre exports crate-root, la redaction, le dependency firewall et l’absence de surface historique/Backfill/Transport/Config/backend direct.
|
||||||
|
|
||||||
|
Le gate technique final `pre.011` passe rustfmt check, audits Rust/Markdown, `cargo check --workspace`, Clippy strict, 56 tests Worker, `cargo test --workspace --all-targets --all-features`, les arbres Cargo Worker normal/features et `cargo tree --duplicates`. La réconciliation `pre.012` ajoute ensuite les README/USAGE durables du Worker et réaligne l’architecture sur l’état matériel : la fondation `0.3.11` n’a aucun adapter HTTP/WS/Yellowstone productif, aucune API publique d’enqueue/source registration et aucun replay/gap-repair live.
|
||||||
|
|
||||||
|
La trajectoire live reste découpée : `0.3.12` ouvre Yellowstone transactions/blocks/status + hydration HTTP + continuité de run, `0.3.13` ajoute WS standard/Helius/HTTP live et la convergence multi-source, puis `0.3.14` ferme le gap repair/hardening multi-source. `prompts/031-V0_3_12_START_PROMPT.md` ouvre `0.3.12` uniquement depuis le tag stable `v0.3.11`; son `pre.001` doit réauditer les surfaces Yellowstone/Solana/provider actuelles, l’injection Transport, l’hydration, le frontier, `from_slot`/ReplayInfo, les smokes accessibles et le sizing avant toute implémentation lourde.
|
||||||
|
|
||||||
|
## 0.3.10 — Lower-layer RAW Transaction commune + qualification cross-source — 2026-09-08
|
||||||
|
|
||||||
|
`0.3.10` introduit `ksp-raw-transaction-lib` comme lower-layer source-neutral commune aux producteurs `RawTransaction`. La canonicalisation RAW v1 auparavant locale à `ksp-job-backfill-lib` est extraite sans modifier les golden bytes/hash existants ; le Backfill consomme désormais cette common crate sans créer de dépendance vers un Worker. La bibliothèque construit les modèles `RawTransaction`/`RawTransactionObservation` de `ksp-store-api`, conserve l'identité canonique `(network, signature)`, borne et redacted les entrées hostiles, et reste volontairement sans Transport, Config, runtime async, Job, Worker ou backend Store physique.
|
||||||
|
|
||||||
|
La common matérialise également un wire Solana source-neutral Legacy/V0/V1 avec sérialisation Base64 et extraction de signature depuis le matériau transactionnel complet. La représentation V1 couvre header, compiled instructions, configuration V1 et signatures terminales ; les gardes structurelles rejettent les états non supportés plutôt que d'inventer une compatibilité. `ksp-onchain-transport-lib` ajoute `get_block_observed`, qui conserve l'identité sûre de la route réellement gagnante tout en réutilisant la validation `getBlock` existante.
|
||||||
|
|
||||||
|
Les preuves cross-source ferment explicitement ce qui est qualifié et ce qui exige encore hydration. Un `blockSubscribe` standard `full/base64` peut produire le même RAW v1 qu'un `getBlock` HTTP sur le sous-ensemble déterministe prouvé. Helius `transactionSubscribe` reste un signal nécessitant hydration HTTP lorsque `blockTime`/version manquent. Yellowstone qualifie le wire Transaction V1 et la conservation du `block_time` dans Block, mais `Transaction` nécessite hydration pour le temps de bloc et le chemin Block complet reste conservateur tant que l'identité byte-for-byte entre meta protobuf et meta JSON HTTP n'est pas prouvée. Aucun RAW-direct Yellowstone complet ni feed EARLY n'est revendiqué sans preuve.
|
||||||
|
|
||||||
|
Le gate technique final `pre.007` passe les audits Rust/Markdown, `cargo check --workspace`, Clippy workspace/all-targets/all-features avec `-D warnings`, `cargo test --workspace --all-targets --all-features`, les suites ciblées common/Transport/Backfill et les graphes Cargo demandés ; les smokes live opt-in restent ignorés conformément à leur contrat. La réconciliation `pre.008` aligne ensuite la terminologie durable des couches Store sur `D1 RAW -> D2 STRUCTURAL -> D3 DECODED -> D4 DOMAIN`, réserve `N1–N4` aux niveaux architecturaux et complète `README.md`/`USAGE.md` de `ksp-raw-transaction-lib`. Son gate opérateur repasse format, audits, `cargo check --workspace` et Clippy strict sur `0.3.10-pre.8`.
|
||||||
|
|
||||||
|
Le runtime concret a été volontairement retiré de `0.3.10` après recalibrage du sizing. La trajectoire devient `0.3.11` fondation runtime/persistence déterministe, `0.3.12` Yellowstone + hydration/continuité, `0.3.13` WS standard/Helius/HTTP live + convergence multi-source, `0.3.14` gap repair/hardening/smokes, `0.3.15` Desk d'ingestion et `0.3.16` extension Backfill multi-source. Le Job historique paramétré et le Worker continu restent des producteurs indépendants du même Store.
|
||||||
|
|
||||||
|
`prompts/030-V0_3_11_START_PROMPT.md` ouvre `0.3.11` uniquement depuis le tag stable `v0.3.10`. Son `pre.001` doit auditer la base réelle, le graphe de dépendances, le runtime/supervision, les bornes de channels, la persistence/déduplication déterministe, les snapshots et le dimensionnement avant toute implémentation lourde ; aucune source live complexe n'est requise pour fermer cette release.
|
||||||
|
|
||||||
|
## 0.3.9 — Worker API générique + audit exhaustif d'acquisition RawTransaction — 2026-09-05
|
||||||
|
|
||||||
|
`0.3.9` introduit `ksp-worker-api` comme façade générique, courte et runtime-neutral pour services continus. La surface stable reste Core-only et expose `WorkerId`, `WorkerKindCode`, `WorkerState`, `WorkerHealth`, `WorkerActivity`, `WorkerLifecycle`, `WorkerStopToken`, `WorkerSnapshotSequence`, `WorkerSnapshot`, `WorkerSnapshotFuture` et `WorkerSnapshotSource`. Les identités sont bornées et redacted, le lifecycle possède des transitions explicites avec terminaux immuables, le stop token est partagé/idempotent, la séquence ne wrappe pas et le snapshot source latest-value reste object-safe, `Send + Sync` et implémentable depuis l'extérieur. L'API ne possède aucun runtime `start/stop` universel, aucune sémantique Job/checkpoint, aucun domaine Solana et aucune dépendance Transport/Store/Config/Tauri.
|
||||||
|
|
||||||
|
Après freeze de cette API, la seconde moitié de la release réalise puis consolide l'audit exhaustif des voies d'acquisition `RawTransaction`. Le document durable `docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md` est réécrit comme une synthèse unique Store-centrique : `RawTransaction` est le cœur RAW durable, tandis que `RawTransactionObservation` conserve les provenances multiples. L'audit distingue systématiquement possibilité protocolaire/provider, support architectural KSP, support implémenté et preuve live ; il couvre HTTP, WebSocket standard, Helius `transactionSubscribe`, Yellowstone gRPC, acquisition par blocs/slots, discovery + hydration, replay/gap repair, archives et sources EARLY/pre-execution. Les matrices recensent aussi les principaux providers/réseaux/tier/coûts connus sans réduire l'architecture aux seuls comptes gratuits actuellement disponibles.
|
||||||
|
|
||||||
|
La séparation des producteurs RAW est figée. `ksp-job-backfill-lib` reste un producteur historique **paramétré, borné et terminable** ; le futur `ksp-worker-raw-transaction-ingest-lib` sera un producteur live **continu start/stop sans requête métier historique**. Ils ne s'appellent pas, ne se supervisent pas et ne collaborent pas : chacun alimente indépendamment le même Store. HTTP, WS, gRPC, replay et archive sont des capabilities orthogonales à ces rôles ; un Worker peut utiliser HTTP pour hydration ou réparation de sa propre continuité live, tandis qu'un Job pourra utiliser gRPC/replay si une campagne historique bornée le justifie. Le handoff `0.3.10` retient `ksp-raw-transaction-lib` comme lower-layer source-neutral commune afin d'extraire la canonicalisation RAW v1 aujourd'hui locale au Backfill sans duplication ni edge Job ↔ Worker ; les golden bytes/hash RAW v1 doivent rester inchangés.
|
||||||
|
|
||||||
|
La release normalise aussi l'identité réseau KSP : `mainnet` devient l'identité canonique dans Config/Store/Transport/tests et `mainnet-beta` reste seulement un alias legacy/externe lorsque la frontière provider l'exige. Les données Mainnet N1 encore utilisées comme données de test ne dictent aucune compatibilité durable et aucune migration SQL n'est introduite pour préserver l'ancien libellé. Pendant le gate final, les baselines workspace sont également relevées à `jsonschema ^0.53` et `yellowstone-grpc-proto ^12.7`; Yellowstone 12.7 ajoute le RPC serveur `SubscribeGossip`, ce qui nécessite uniquement l'adaptation des fixtures `Geyser` avec une réponse `UNIMPLEMENTED` et n'ouvre aucune capability Gossip de production.
|
||||||
|
|
||||||
|
Le gate technique final passe les audits Rust/Markdown, `cargo check --workspace`, Clippy workspace/all-targets/all-features avec `-D warnings`, 385/385 tests unitaires `ksp-onchain-transport-lib`, 43/43 canaries `release_completeness`, `cargo test --workspace --all-targets --all-features`, le bundle complet `ksp-worker-api` et les graphes Cargo jusqu'à `cargo tree --duplicates`; seuls les smokes/probes explicitement opt-in restent ignorés. La réconciliation documentaire `pre.008` réaligne ensuite README/USAGE/indexes/architecture/plan/validation et son gate opérateur repasse audits Rust/Markdown plus `cargo check --workspace` en `0.3.9-pre.8`.
|
||||||
|
|
||||||
|
`prompts/029-V0_3_10_START_PROMPT.md` ouvre `0.3.10` exclusivement depuis le tag stable `v0.3.9`. La prochaine release doit d'abord matérialiser `ksp-raw-transaction-lib`, migrer le Backfill vers cette canonicalisation commune sans changement fonctionnel, puis construire `ksp-worker-raw-transaction-ingest-lib` multi-source dès V1. Son `pre.001` est obligatoirement un gate d'audit/sizing : inventaire exact des gaps Transport/Config, runtime Worker, capabilities live, continuité, preuves provider et dépendances avant toute implémentation lourde.
|
||||||
|
|
||||||
## 0.3.8 — Store Desk V1 RAW, inspection backend-neutral et trajectoire Worker multi-source — 2026-09-04
|
## 0.3.8 — Store Desk V1 RAW, inspection backend-neutral et trajectoire Worker multi-source — 2026-09-04
|
||||||
|
|
||||||
`0.3.8` introduit `ksp-app-store-desk`, application Tauri KSP read-only dédiée à l'inspection du Store. Le package reprend le gabarit Desk commun (splash/shell/assets/styles/tracing), compose `ksp-config-lib`, `ksp-logging-lib` et la seule façade `ksp-store-lib`, et interdit à l'application tout accès direct à `ksp-store-api`, `ksp-store-postgres-lib`, SQL, driver/pool PostgreSQL, Transport on-chain ou ressource physique. Le bootstrap ouvre un Store lié au profil/réseau composite, expose health/runtime backend-neutral et effectue un shutdown borné à la fermeture de la fenêtre principale.
|
`0.3.8` introduit `ksp-app-store-desk`, application Tauri KSP read-only dédiée à l'inspection du Store. Le package reprend le gabarit Desk commun (splash/shell/assets/styles/tracing), compose `ksp-config-lib`, `ksp-logging-lib` et la seule façade `ksp-store-lib`, et interdit à l'application tout accès direct à `ksp-store-api`, `ksp-store-postgres-lib`, SQL, driver/pool PostgreSQL, Transport on-chain ou ressource physique. Le bootstrap ouvre un Store lié au profil/réseau composite, expose health/runtime backend-neutral et effectue un shutdown borné à la fermeture de la fenêtre principale.
|
||||||
|
|||||||
10
Cargo.toml
10
Cargo.toml
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 475
|
# version: 637
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
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-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-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]
|
[workspace.package]
|
||||||
version = "0.3.8"
|
version = "0.3.15"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
@@ -25,7 +25,7 @@ fs2 = { version = "^0.4" }
|
|||||||
futures-util = { version = "^0.3", default-features = false }
|
futures-util = { version = "^0.3", default-features = false }
|
||||||
getrandom = { version = "^0.4", default-features = false }
|
getrandom = { version = "^0.4", default-features = false }
|
||||||
http = { version = "^1.5", default-features = false }
|
http = { version = "^1.5", default-features = false }
|
||||||
jsonschema = { version = "^0.52", default-features = false }
|
jsonschema = { version = "^0.53", default-features = false }
|
||||||
reqwest = { version = "^0.13", default-features = false }
|
reqwest = { version = "^0.13", default-features = false }
|
||||||
rustls = { version = "^0.23", default-features = false }
|
rustls = { version = "^0.23", default-features = false }
|
||||||
rustls-native-certs = { version = "^0.8", default-features = false }
|
rustls-native-certs = { version = "^0.8", default-features = false }
|
||||||
@@ -49,7 +49,7 @@ tokio-tungstenite = { version = "^0.30", default-features = false }
|
|||||||
tonic = { version = "^0.14", default-features = false }
|
tonic = { version = "^0.14", default-features = false }
|
||||||
tonic-prost = { version = "^0.14", default-features = false }
|
tonic-prost = { version = "^0.14", default-features = false }
|
||||||
ts-rs = { version = "^12.0" }
|
ts-rs = { version = "^12.0" }
|
||||||
yellowstone-grpc-proto = { version = "^12.6", default-features = false }
|
yellowstone-grpc-proto = { version = "^12.7", default-features = false }
|
||||||
zeroize = { version = "^1.9" }
|
zeroize = { version = "^1.9" }
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: README.md -->
|
<!-- file: README.md -->
|
||||||
<!-- version: 10 -->
|
<!-- version: 15 -->
|
||||||
|
|
||||||
# Khadhroony Solana Project
|
# Khadhroony Solana Project
|
||||||
|
|
||||||
@@ -55,7 +55,11 @@ La couche RAW dispose d'une façade Store backend-neutral et d'un premier job hi
|
|||||||
|
|
||||||
`ksp-app-store-desk` fournit l'inspection desktop read-only du Store RAW via `ksp-store-lib` : health/runtime backend-neutral, tables server-side `RawTransaction` et `RawAccountState`, observations associées, détails avec previews bornées et lecture des états de rétention/tombstones. DataTables possède l'unique pagination visible de l'inspection random-access ; la pagination cursor/keyset des consumers machine reste distincte et intacte.
|
`ksp-app-store-desk` fournit l'inspection desktop read-only du Store RAW via `ksp-store-lib` : health/runtime backend-neutral, tables server-side `RawTransaction` et `RawAccountState`, observations associées, détails avec previews bornées et lecture des états de rétention/tombstones. DataTables possède l'unique pagination visible de l'inspection random-access ; la pagination cursor/keyset des consumers machine reste distincte et intacte.
|
||||||
|
|
||||||
Ces contrats restent distincts des futurs workers continus : un job borné n'est ni un service worker ni un pipeline générique imposé aux autres couches.
|
`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 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
|
## Points d'entrée
|
||||||
|
|
||||||
|
|||||||
38
ROADMAP.md
38
ROADMAP.md
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: ROADMAP.md -->
|
<!-- file: ROADMAP.md -->
|
||||||
<!-- version: 103 -->
|
<!-- version: 114 -->
|
||||||
|
|
||||||
# Roadmap KSP
|
# Roadmap KSP
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
|
|||||||
|
|
||||||
### Cadrage
|
### Cadrage
|
||||||
|
|
||||||
- [X] `0.2.0` — Audit bot3, ordre fonctionnel de `0.2.x`, architecture durable, discipline de sizing et pipeline RAW/CORE/DECODE/SPECIALIZED stabilisés.
|
- [X] `0.2.0` — Audit bot3, ordre fonctionnel de `0.2.x`, architecture durable, discipline de sizing et pipeline RAW/STRUCTURAL/DECODED/DOMAIN stabilisés.
|
||||||
- [X] `0.2.1` — HTTP foundation stable : matrice 52+14, runtime/routing/résilience/exécution HTTP, 4 canaris typés, `std.transport`, adapter Config -> Transport, canaries de clôture, smoke Devnet opt-in et documentation durable validés ; les 48 wrappers typés restants sont reportés à `0.2.2`–`0.2.4`.
|
- [X] `0.2.1` — HTTP foundation stable : matrice 52+14, runtime/routing/résilience/exécution HTTP, 4 canaris typés, `std.transport`, adapter Config -> Transport, canaries de clôture, smoke Devnet opt-in et documentation durable validés ; les 48 wrappers typés restants sont reportés à `0.2.2`–`0.2.4`.
|
||||||
|
|
||||||
### Releases fonctionnelles décidées/pressenties
|
### Releases fonctionnelles décidées/pressenties
|
||||||
@@ -87,13 +87,13 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
|
|||||||
```
|
```
|
||||||
|
|
||||||
- **RAW** et **STRUCTURAL** ne nécessitent aucun décodage Program.
|
- **RAW** et **STRUCTURAL** ne nécessitent aucun décodage Program.
|
||||||
- La progression n'est pas une chaîne obligatoire pour chaque famille : une donnée event-only peut s'arrêter en N1, et un état de compte pourra aller directement vers un futur decoder si aucune décomposition STRUCTURAL utile n'existe.
|
- La progression n'est pas une chaîne obligatoire pour chaque famille : une donnée event-only peut s'arrêter en D1, et un état de compte pourra aller directement vers un futur decoder si aucune décomposition STRUCTURAL utile n'existe.
|
||||||
- À la fin des couches horizontales RAW/STRUCTURAL réellement persistées, ajouter les jobs/workers/apps nécessaires avant d'ouvrir la couche suivante.
|
- À la fin des couches horizontales RAW/STRUCTURAL réellement persistées, ajouter les jobs/workers/apps nécessaires avant d'ouvrir la couche suivante.
|
||||||
- À partir de **DECODED**, avancer verticalement groupe par groupe ; **DOMAIN** désigne les projections métier et la matérialisation est le processus qui les produit.
|
- À partir de **DECODED**, avancer verticalement groupe par groupe ; **DOMAIN** désigne les projections métier et la matérialisation est le processus qui les produit.
|
||||||
|
|
||||||
## 0.3.x — RAW / acquisition persistée
|
## 0.3.x — RAW / acquisition persistée
|
||||||
|
|
||||||
- [X] `0.3.1` — `ksp-store-api` stable : modèles N1 RAW backend-agnostic `RawTransaction` et `RawAccountState` avec observations, provenance, payload/hash/timestamps bornés, 10 capabilities object-safe, queries cursorisées sans plafond métier arbitraire, outcomes idempotence/conflit et lifecycle logique rétention/tombstone/force-rehydrate ; aucun backend physique, Config, runtime Store, notification dédiée ni surface STRUCTURAL/DECODED/DOMAIN.
|
- [X] `0.3.1` — `ksp-store-api` stable : modèles D1 RAW backend-agnostic `RawTransaction` et `RawAccountState` avec observations, provenance, payload/hash/timestamps bornés, 10 capabilities object-safe, queries cursorisées sans plafond métier arbitraire, outcomes idempotence/conflit et lifecycle logique rétention/tombstone/force-rehydrate ; aucun backend physique, Config, runtime Store, notification dédiée ni surface STRUCTURAL/DECODED/DOMAIN.
|
||||||
- [X] `0.3.2` — `ksp-store-lib` + `ksp-store-postgres-lib` stables comme fondation runtime/backend PostgreSQL : feature `postgres` par défaut, `Store` lié à un unique `RawNetworkId`, Config `std.store` avec targets/bases `devnet`/`mainnet`/`testnet`, pool Deadpool borné, `tokio-postgres`, TLS Rustls `Disabled`/`VerifyFull`, moteur de migrations privé `V000` + SHA-256/advisory lock, health/readiness portable et close borné. Gate complet + PostgreSQL réel major 17 verts ; aucune table/capability `RawTransaction`/`RawAccountState` métier n'est encore ajoutée.
|
- [X] `0.3.2` — `ksp-store-lib` + `ksp-store-postgres-lib` stables comme fondation runtime/backend PostgreSQL : feature `postgres` par défaut, `Store` lié à un unique `RawNetworkId`, Config `std.store` avec targets/bases `devnet`/`mainnet`/`testnet`, pool Deadpool borné, `tokio-postgres`, TLS Rustls `Disabled`/`VerifyFull`, moteur de migrations privé `V000` + SHA-256/advisory lock, health/readiness portable et close borné. Gate complet + PostgreSQL réel major 17 verts ; aucune table/capability `RawTransaction`/`RawAccountState` métier n'est encore ajoutée.
|
||||||
- [X] `0.3.3` — Vertical slice PostgreSQL `RawTransaction` complète sur `ksp-store-lib` + `ksp-store-postgres-lib` : six capabilities transaction/observation/rétention, V001 physique liée à un réseau, acquisition canonical+observation atomique, idempotence/conflit, get/list keyset cursorisé, archive/purge/tombstone/ForceRehydrate, hardening des erreurs et du schéma, concurrence et rollback validés sur PostgreSQL 17.
|
- [X] `0.3.3` — Vertical slice PostgreSQL `RawTransaction` complète sur `ksp-store-lib` + `ksp-store-postgres-lib` : six capabilities transaction/observation/rétention, V001 physique liée à un réseau, acquisition canonical+observation atomique, idempotence/conflit, get/list keyset cursorisé, archive/purge/tombstone/ForceRehydrate, hardening des erreurs et du schéma, concurrence et rollback validés sur PostgreSQL 17.
|
||||||
- [X] `0.3.4` — Vertical slice PostgreSQL `RawAccountState` complète sur `ksp-store-lib` + `ksp-store-postgres-lib` : quatre capabilities account ajoutées aux six transaction pour une conformance RAW 10/10, V002 additive de 32 ressources au-dessus de V000/V001 immuables, state+observation atomiques, idempotence/conflit exacts, metadata Yellowstone observation-only, get/list keyset `(slot,pubkey,state_hash)` avec cursor KSPA anti-replay, hardening cross-family et live validé sur PostgreSQL 17 sans rétention destructive account.
|
- [X] `0.3.4` — Vertical slice PostgreSQL `RawAccountState` complète sur `ksp-store-lib` + `ksp-store-postgres-lib` : quatre capabilities account ajoutées aux six transaction pour une conformance RAW 10/10, V002 additive de 32 ressources au-dessus de V000/V001 immuables, state+observation atomiques, idempotence/conflit exacts, metadata Yellowstone observation-only, get/list keyset `(slot,pubkey,state_hash)` avec cursor KSPA anti-replay, hardening cross-family et live validé sur PostgreSQL 17 sans rétention destructive account.
|
||||||
@@ -101,22 +101,28 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
|
|||||||
- [X] `0.3.6` — `ksp-job-api` + `ksp-job-backfill-lib` stables pour la première verticale historique `RawTransaction` : lifecycle/cancellation/latest-value runtime-neutral, scopes `Latest`/`Before`/`After`/signatures explicites, découverte/hydratation Transport observée, RAW v1 canonique, persistance Store atomique/idempotente, concurrence bornée, frontier contiguë, checkpoint/reprise caller-owned, snapshots sûrs et hardening externe sans dépendance backend/provider directe.
|
- [X] `0.3.6` — `ksp-job-api` + `ksp-job-backfill-lib` stables pour la première verticale historique `RawTransaction` : lifecycle/cancellation/latest-value runtime-neutral, scopes `Latest`/`Before`/`After`/signatures explicites, découverte/hydratation Transport observée, RAW v1 canonique, persistance Store atomique/idempotente, concurrence bornée, frontier contiguë, checkpoint/reprise caller-owned, snapshots sûrs et hardening externe sans dépendance backend/provider directe.
|
||||||
- [X] `0.3.7` — `ksp-app-backfill-desk` livrée comme Desk Tauri KSP spécialisée : composition Config Mainnet/Devnet/Testnet, quatre scopes HTTP, validation/start single-run, monitoring latest-value `ksp-backfill-status`, Cancel ciblé/idempotent, Resume in-session par checkpoint Rust-only réémis pour un nouveau JobId, autocomplete libre depuis `ksp-core-lib`, hardening IPC/dependency boundaries et build Linux `.deb`/`.rpm`/`.AppImage`. Aucun SQL/backend/provider physique ni checkpoint durable n’est exposé.
|
- [X] `0.3.7` — `ksp-app-backfill-desk` livrée comme Desk Tauri KSP spécialisée : composition Config Mainnet/Devnet/Testnet, quatre scopes HTTP, validation/start single-run, monitoring latest-value `ksp-backfill-status`, Cancel ciblé/idempotent, Resume in-session par checkpoint Rust-only réémis pour un nouveau JobId, autocomplete libre depuis `ksp-core-lib`, hardening IPC/dependency boundaries et build Linux `.deb`/`.rpm`/`.AppImage`. Aucun SQL/backend/provider physique ni checkpoint durable n’est exposé.
|
||||||
- [X] `0.3.8` — `ksp-app-store-desk` V1 RAW livrée sur le gabarit KSP courant comme application Tauri read-only backend-neutral. La Desk compose Config + Logging + `ksp-store-lib`, expose health/runtime sûrs, DataTables `serverSide` pour `RawTransaction`, `RawAccountState` et leurs observations, détails bornés, provenance sûre et états de rétention/tombstone, sans SQL/backend physique/Transport dans l'application. La nouvelle inspection random-access `offset + limit + counts exacts` reste distincte de la pagination machine cursor/keyset conservée pour workers/backfills/replays. Le gate final et les builds Linux `.deb`/`.rpm` sont verts.
|
- [X] `0.3.8` — `ksp-app-store-desk` V1 RAW livrée sur le gabarit KSP courant comme application Tauri read-only backend-neutral. La Desk compose Config + Logging + `ksp-store-lib`, expose health/runtime sûrs, DataTables `serverSide` pour `RawTransaction`, `RawAccountState` et leurs observations, détails bornés, provenance sûre et états de rétention/tombstone, sans SQL/backend physique/Transport dans l'application. La nouvelle inspection random-access `offset + limit + counts exacts` reste distincte de la pagination machine cursor/keyset conservée pour workers/backfills/replays. Le gate final et les builds Linux `.deb`/`.rpm` sont verts.
|
||||||
- [ ] `0.3.9` — Introduire `ksp-worker-api` comme API **générique et volontairement courte** de lifecycle/health/progression/snapshot pour services continus, distincte de `ksp-job-api` et sans dépendance Solana/Transport/Store/Tauri. Une fois l'API Worker fonctionnellement fermée, terminer la release par un audit fonctionnel exhaustif des sources/méthodes d'acquisition `RawTransaction` : HTTP, WS standard, extensions provider, Yellowstone gRPC, blocks/slots, discovery + hydration, replay/gap-repair et combinaisons multi-provider. Cet audit prépare `0.3.10` sans ajouter d'endpoint ni de Config provider dans `0.3.9`.
|
- [X] `0.3.9` — `ksp-worker-api` stable comme API générique et volontairement courte pour services continus : identité/kind bornés, lifecycle explicite, health/activity sûrs, stop token partagé, séquence/snapshot latest-value et `WorkerSnapshotSource` object-safe, avec dépendance Core-only et sans runtime/start-stop universel, Solana, Transport, Store, Config ou Tauri. Après freeze Worker API, l'audit `RawTransaction` a été consolidé dans une synthèse Store-centrique exhaustive séparant possibilités/support/preuve et classant HTTP, WS standard/provider, Yellowstone gRPC, blocs/slots, discovery + hydration, replay de continuité, archives et sources EARLY. Le Job Backfill historique paramétré et le Worker Ingest live start/stop sont deux producteurs indépendants du même Store ; `mainnet` est désormais l'identité KSP canonique et `mainnet-beta` un alias legacy/externe. Le handoff retient `ksp-raw-transaction-lib` comme lower-layer commune de canonicalisation RAW v1 pour `0.3.10`, sans edge Job ↔ Worker.
|
||||||
- [ ] `0.3.10` — Introduire `ksp-worker-raw-transaction-ingest-lib` **multi-source dès V1**. Le worker compose les stratégies retenues par l'audit `0.3.9` comme sources alternatives, complémentaires, redondantes ou spécialisées live/catch-up/gap-repair, converge vers `RawTransaction + RawTransactionObservation`, persiste par `ksp-store-lib` et utilise `ksp-worker-api` pour supervision/cancellation. Cette release porte aussi, et seulement si l'audit les justifie, les adaptations nécessaires de `ksp-onchain-transport-lib`/`ksp-config-lib` : futures sources Helius HTTP/WS Mainnet/Devnet avec réutilisation de `KSP_SECRET_HELIUS_API_KEY`, capabilities/tier réaudités au moment du travail, et éventuelle canonicalisation `mainnet`/`mainnet-beta` uniquement avec stratégie de compatibilité sûre. Aucun decode Program ni dépendance backend/provider directe dans le worker.
|
- [X] `0.3.10` — Lower-layer RAW Transaction commune stabilisée : `ksp-raw-transaction-lib` possède la canonicalisation RAW v1 source-neutral partagée, le Backfill est migré sans changement des golden bytes/hash, Transport expose `get_block_observed`, le wire Solana Legacy/V0/V1 est matérialisé et les canaris HTTP/WS standard/Helius/Yellowstone qualifient précisément les voies directes ou nécessitant hydration. La release se ferme sans runtime Worker concret ; celui-ci commence en `0.3.11` selon le redécoupage `0.3.11`–`0.3.14`.
|
||||||
- [ ] `0.3.11` — 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 `0.3.10` : 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 ; l'inspection détaillée des RAW persistés reste la responsabilité de `ksp-app-store-desk`.
|
- [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`.
|
||||||
- [ ] `0.3.12` — É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 (par exemple block/slot, replay provider lorsqu'il existe), sans supposer qu'une source live WS constitue un historique universel.
|
- [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.
|
||||||
|
- [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
|
### TODO/IDEAS — applications spécialisées et control plane
|
||||||
|
|
||||||
- [ ] **TODO** — faire évoluer `ksp-app-store-desk` avec chaque nouvelle couche réellement persistée : RAW d'abord, puis STRUCTURAL, DECODED, journal de processing/materialization et projections DOMAIN selon les contrats effectivement disponibles. Les vues avancées restent backend-agnostiques et passent uniquement par `ksp-store-lib`.
|
- [ ] **TODO** — faire évoluer `ksp-app-store-desk` avec chaque nouvelle couche réellement persistée : RAW d'abord, puis STRUCTURAL, DECODED, journal de processing/materialization et projections DOMAIN selon les contrats effectivement disponibles. Les vues avancées restent backend-agnostiques et passent uniquement par `ksp-store-lib`.
|
||||||
- [ ] **TODO** — différer une future `ksp-app-control-desk` jusqu'à ce que KSP dispose au minimum d'un niveau N3/D3 de processing/materialization exploitable et de plusieurs decoders réels. Cette application sera une surface **end-user simplifiée** : démarrer/arrêter les flux autorisés, lancer une recherche courante, voir l'état global et les erreurs importantes. Elle ne recopiera pas le diagnostic détaillé, les réglages avancés, les tables complètes ni toutes les fonctions des `ksp-app-*-desk` spécialisées, qui resteront les outils d'administration, développement et investigation approfondie.
|
- [ ] **TODO** — différer une future `ksp-app-control-desk` jusqu'à ce que KSP dispose au minimum d'une couche D3 DECODED de processing/materialization exploitable et de plusieurs decoders réels. Cette application sera une surface **end-user simplifiée** : démarrer/arrêter les flux autorisés, lancer une recherche courante, voir l'état global et les erreurs importantes. Elle ne recopiera pas le diagnostic détaillé, les réglages avancés, les tables complètes ni toutes les fonctions des `ksp-app-*-desk` spécialisées, qui resteront les outils d'administration, développement et investigation approfondie.
|
||||||
|
|
||||||
### TODO/IDEAS — taxonomie N1, processing et rétention
|
### TODO/IDEAS — taxonomie D1, processing et rétention
|
||||||
|
|
||||||
- [ ] **TODO** — maintenir la matrice d’admission HTTP/WS/gRPC/provider lors de toute nouvelle famille N1 : plusieurs sources ne convergent vers un même struct que si elles satisfont la même sémantique sans perte.
|
- [ ] **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.
|
||||||
- [ ] **TODO `0.3.9`** — produire après fermeture de `ksp-worker-api` une matrice RAW Transaction unique et réutilisable par `0.3.10` puis `0.3.12`, couvrant discovery, hydration/direct payload, filtres, ordering/duplicates, reconnect/replay, gap repair, backpressure, commitment, provenance, quotas, gaps Transport/Config et applicability continuous-ingest/catch-up/backfill.
|
- [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.
|
||||||
- [ ] **TODO Helius futur** — ne pas ajouter d'URL/endpoints Helius en `0.3.8` ni pendant la construction générique de Worker API. L'audit `0.3.9` réévalue les capabilities/tier courants ; `0.3.10` peut ensuite ajouter les profils HTTP/WS réellement nécessaires en réutilisant exclusivement `KSP_SECRET_HELIUS_API_KEY` via Config.
|
- [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.
|
||||||
- [ ] **TODO réseau** — auditer `mainnet` vs `mainnet-beta` avant tout renommage : Store persisté, Config, Transport, checkpoints/fingerprints et aliases externes doivent converger vers une identité KSP unique pour le même cluster ; aucune migration/canonicalisation n'est supposée avant cette preuve.
|
- [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.
|
- [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.
|
||||||
- [ ] **TODO** — statut/commitment transactionnel restant : `0.3.5` couvre uniquement le fait passif d’exécution `slot + signature + outcome`; réauditer séparément `signatureSubscribe` et `getSignatureStatuses` lorsqu’un consumer de commitment/snapshot réel apparaît, sans fusionner snapshot, transition et execution update dans un modèle Option-soup.
|
- [ ] **TODO** — statut/commitment transactionnel restant : `0.3.5` couvre uniquement le fait passif d’exécution `slot + signature + outcome`; réauditer séparément `signatureSubscribe` et `getSignatureStatuses` lorsqu’un consumer de commitment/snapshot réel apparaît, sans fusionner snapshot, transition et execution update dans un modèle Option-soup.
|
||||||
- [ ] **IDEA** — logs realtime enrichis : `logsSubscribe` alimente déjà la projection minimale `TransactionExecutionEvent`, mais un éventuel `TransactionLogEvent` portant les lignes de log reste différé dans `docs/IDEAS.md` jusqu’à démonstration d’un consumer et de bornes explicites. `logMessages` reste dans `RawTransaction` jusqu’à STRUCTURAL ; le wake-up post-commit reste distinct et Store API-owned conformément à `KSP-NOTIFY-*`.
|
- [ ] **IDEA** — logs realtime enrichis : `logsSubscribe` alimente déjà la projection minimale `TransactionExecutionEvent`, mais un éventuel `TransactionLogEvent` portant les lignes de log reste différé dans `docs/IDEAS.md` jusqu’à démonstration d’un consumer et de bornes explicites. `logMessages` reste dans `RawTransaction` jusqu’à STRUCTURAL ; le wake-up post-commit reste distinct et Store API-owned conformément à `KSP-NOTIFY-*`.
|
||||||
@@ -129,13 +135,13 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
|
|||||||
- [X] rétention physique `RawTransaction` PostgreSQL — `0.3.3` matérialise `Full -> Archived -> Purged`, tombstone et ForceRehydrate atomiques ; `Compacted` reste explicitement unsupported tant qu’aucune représentation compactée réelle n’existe.
|
- [X] rétention physique `RawTransaction` PostgreSQL — `0.3.3` matérialise `Full -> Archived -> Purged`, tombstone et ForceRehydrate atomiques ; `Compacted` reste explicitement unsupported tant qu’aucune représentation compactée réelle n’existe.
|
||||||
- [ ] **TODO** — policy de rétention/compaction : définir les critères d’éligibilité fondés sur les preuves de processing et la maintenance worker/job ; Store applique une transition demandée mais ne décide pas seul qu’un RAW peut être archivé/purgé, et la compaction physique ne sera ajoutée qu’avec un besoin réel.
|
- [ ] **TODO** — policy de rétention/compaction : définir les critères d’éligibilité fondés sur les preuves de processing et la maintenance worker/job ; Store applique une transition demandée mais ne décide pas seul qu’un RAW peut être archivé/purgé, et la compaction physique ne sera ajoutée qu’avec un besoin réel.
|
||||||
- [X] frontière `ksp-interface-lib` / `ksp-store-api` — ownership documenté et canaris de non-duplication stabilisés en `0.3.1`; les events passifs non persistés restent Interface, les modèles persistants/replayables restent Store API.
|
- [X] frontière `ksp-interface-lib` / `ksp-store-api` — ownership documenté et canaris de non-duplication stabilisés en `0.3.1`; les events passifs non persistés restent Interface, les modèles persistants/replayables restent Store API.
|
||||||
- [ ] **IDEA** — réauditer la structure de processing/decode/materialization historique kbot2/kbot3 lors de l’ouverture de N2/N3 ; conserver l’isolation instruction/CPI et les statuts terminal/versionnés, sans reprendre automatiquement le schéma SQL historique.
|
- [ ] **IDEA** — réauditer la structure de processing/decode/materialization historique kbot2/kbot3 lors de l’ouverture de D2/D3 ; conserver l’isolation instruction/CPI et les statuts terminal/versionnés, sans reprendre automatiquement le schéma SQL historique.
|
||||||
|
|
||||||
## Série STRUCTURAL suivante
|
## Série STRUCTURAL suivante
|
||||||
|
|
||||||
- [ ] Définir la persistence STRUCTURAL canonique Solana générique pour les familles réellement décomposables.
|
- [ ] Définir la persistence STRUCTURAL canonique Solana générique pour les familles réellement décomposables.
|
||||||
- [ ] Implémenter en priorité `RawTransaction -> STRUCTURAL` sans decoder Program : transaction/message, comptes/références, instructions top-level, CPI/inner instructions, logs/meta/balances/return data et relations structurelles.
|
- [ ] Implémenter en priorité `RawTransaction -> STRUCTURAL` sans decoder Program : transaction/message, comptes/références, instructions top-level, CPI/inner instructions, logs/meta/balances/return data et relations structurelles.
|
||||||
- [ ] Vérifier avant extension si d'autres familles N1 possèdent une vraie décomposition STRUCTURAL utile ; ne pas créer de niveau vide par convention.
|
- [ ] Vérifier avant extension si d'autres familles D1 possèdent une vraie décomposition STRUCTURAL utile ; ne pas créer de niveau vide par convention.
|
||||||
- [ ] Ajouter replay/backfill RAW -> STRUCTURAL avec processing versionné.
|
- [ ] Ajouter replay/backfill RAW -> STRUCTURAL avec processing versionné.
|
||||||
- [ ] Ajouter le worker/service STRUCTURAL utile sans le coupler à un worker RAW concret.
|
- [ ] Ajouter le worker/service STRUCTURAL utile sans le coupler à un worker RAW concret.
|
||||||
- [ ] Faire évoluer `ksp-app-store-desk` avec des vues/requêtes STRUCTURAL lorsque cette couche est réellement persistée ; ne pas créer une seconde application de browsing des mêmes données.
|
- [ ] Faire évoluer `ksp-app-store-desk` avec des vues/requêtes STRUCTURAL lorsque cette couche est réellement persistée ; ne pas créer une seconde application de browsing des mêmes données.
|
||||||
|
|||||||
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"profile_id": "mainnet",
|
"profile_id": "mainnet",
|
||||||
"network": "mainnet-beta",
|
"network": "mainnet",
|
||||||
"backend": "postgres",
|
"backend": "postgres",
|
||||||
"postgres": {
|
"postgres": {
|
||||||
"connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}",
|
"connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}",
|
||||||
|
|||||||
@@ -31,8 +31,8 @@
|
|||||||
"name": "mainnet_public",
|
"name": "mainnet_public",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"provider": "solana-public",
|
"provider": "solana-public",
|
||||||
"cluster": "mainnet-beta",
|
"cluster": "mainnet",
|
||||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet-beta.solana.com}",
|
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet.solana.com}",
|
||||||
"connect_timeout_ms": 5000,
|
"connect_timeout_ms": 5000,
|
||||||
"request_timeout_ms": 15000,
|
"request_timeout_ms": 15000,
|
||||||
"max_idle_connections_per_host": 8,
|
"max_idle_connections_per_host": 8,
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
"name": "mainnet_private",
|
"name": "mainnet_private",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"provider": "private-provider",
|
"provider": "private-provider",
|
||||||
"cluster": "mainnet-beta",
|
"cluster": "mainnet",
|
||||||
"url": "${KSP_SECRET_SOLANA_HTTP_URL:-https://example.invalid}",
|
"url": "${KSP_SECRET_SOLANA_HTTP_URL:-https://example.invalid}",
|
||||||
"connect_timeout_ms": 5000,
|
"connect_timeout_ms": 5000,
|
||||||
"request_timeout_ms": 10000,
|
"request_timeout_ms": 10000,
|
||||||
@@ -85,21 +85,39 @@
|
|||||||
"name": "mainnet_public_ws",
|
"name": "mainnet_public_ws",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"provider": "solana-public",
|
"provider": "solana-public",
|
||||||
"cluster": "mainnet-beta",
|
"cluster": "mainnet",
|
||||||
"kind": "solana_standard",
|
"kind": "solana_standard",
|
||||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.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",
|
"name": "mainnet_helius_ws",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"provider": "helius",
|
"provider": "helius",
|
||||||
"cluster": "mainnet-beta",
|
"cluster": "mainnet",
|
||||||
"kind": "helius_laserstream",
|
"kind": "helius_laserstream",
|
||||||
"url": "wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}",
|
"url": "wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}",
|
||||||
"session": {
|
"session": {
|
||||||
"notification_queue_capacity": 512,
|
"notification_queue_capacity": 512,
|
||||||
"max_active_subscriptions": 2048
|
"max_active_subscriptions": 2048
|
||||||
}
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"account",
|
||||||
|
"logs",
|
||||||
|
"program",
|
||||||
|
"root",
|
||||||
|
"signature",
|
||||||
|
"slot",
|
||||||
|
"slots_updates",
|
||||||
|
"helius_transaction"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -140,7 +158,17 @@
|
|||||||
"provider": "helius",
|
"provider": "helius",
|
||||||
"cluster": "devnet",
|
"cluster": "devnet",
|
||||||
"kind": "helius_laserstream",
|
"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": {
|
"session": {
|
||||||
"$ref": "#/$defs/wsSessionOverride"
|
"$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"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"profile_id": "mainnet",
|
"profile_id": "mainnet",
|
||||||
"network": "mainnet-beta",
|
"network": "mainnet",
|
||||||
"backend": "postgres",
|
"backend": "postgres",
|
||||||
"postgres": {
|
"postgres": {
|
||||||
"connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}",
|
"connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}",
|
||||||
|
|||||||
@@ -75,7 +75,67 @@
|
|||||||
"provider": "solana-public",
|
"provider": "solana-public",
|
||||||
"cluster": "devnet",
|
"cluster": "devnet",
|
||||||
"kind": "solana_standard",
|
"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",
|
"provider": "solana-public",
|
||||||
"cluster": "devnet",
|
"cluster": "devnet",
|
||||||
"kind": "solana_standard",
|
"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": [
|
"grpc_endpoints": [
|
||||||
@@ -144,8 +213,8 @@
|
|||||||
"name": "solana_mainnet_public",
|
"name": "solana_mainnet_public",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"provider": "solana-public",
|
"provider": "solana-public",
|
||||||
"cluster": "mainnet-beta",
|
"cluster": "mainnet",
|
||||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet-beta.solana.com}",
|
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet.solana.com}",
|
||||||
"connect_timeout_ms": 5000,
|
"connect_timeout_ms": 5000,
|
||||||
"request_timeout_ms": 15000,
|
"request_timeout_ms": 15000,
|
||||||
"max_idle_connections_per_host": 8,
|
"max_idle_connections_per_host": 8,
|
||||||
@@ -172,9 +241,69 @@
|
|||||||
"name": "solana_mainnet_public_ws",
|
"name": "solana_mainnet_public_ws",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"provider": "solana-public",
|
"provider": "solana-public",
|
||||||
"cluster": "mainnet-beta",
|
"cluster": "mainnet",
|
||||||
"kind": "solana_standard",
|
"kind": "solana_standard",
|
||||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -185,8 +314,8 @@
|
|||||||
"name": "solana_mainnet_public",
|
"name": "solana_mainnet_public",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"provider": "solana-public",
|
"provider": "solana-public",
|
||||||
"cluster": "mainnet-beta",
|
"cluster": "mainnet",
|
||||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet-beta.solana.com}",
|
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet.solana.com}",
|
||||||
"connect_timeout_ms": 5000,
|
"connect_timeout_ms": 5000,
|
||||||
"request_timeout_ms": 15000,
|
"request_timeout_ms": 15000,
|
||||||
"max_idle_connections_per_host": 8,
|
"max_idle_connections_per_host": 8,
|
||||||
@@ -225,7 +354,7 @@
|
|||||||
"name": "publicnode_solana_mainnet_http",
|
"name": "publicnode_solana_mainnet_http",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"provider": "publicnode",
|
"provider": "publicnode",
|
||||||
"cluster": "mainnet-beta",
|
"cluster": "mainnet",
|
||||||
"url": "https://solana-rpc.publicnode.com",
|
"url": "https://solana-rpc.publicnode.com",
|
||||||
"connect_timeout_ms": 5000,
|
"connect_timeout_ms": 5000,
|
||||||
"request_timeout_ms": 15000,
|
"request_timeout_ms": 15000,
|
||||||
@@ -267,9 +396,18 @@
|
|||||||
"name": "solana_mainnet_public_ws",
|
"name": "solana_mainnet_public_ws",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"provider": "solana-public",
|
"provider": "solana-public",
|
||||||
"cluster": "mainnet-beta",
|
"cluster": "mainnet",
|
||||||
"kind": "solana_standard",
|
"kind": "solana_standard",
|
||||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}"
|
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}",
|
||||||
|
"capabilities": [
|
||||||
|
"account",
|
||||||
|
"block",
|
||||||
|
"logs",
|
||||||
|
"program",
|
||||||
|
"root",
|
||||||
|
"signature",
|
||||||
|
"slot"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -277,11 +415,35 @@
|
|||||||
"profile_id": "publicnode_mainnet",
|
"profile_id": "publicnode_mainnet",
|
||||||
"endpoints": [
|
"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,
|
"enabled": true,
|
||||||
"provider": "solana-public",
|
"provider": "solana-public",
|
||||||
"cluster": "mainnet-beta",
|
"cluster": "mainnet",
|
||||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet-beta.solana.com}",
|
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet.solana.com}",
|
||||||
"connect_timeout_ms": 5000,
|
"connect_timeout_ms": 5000,
|
||||||
"request_timeout_ms": 15000,
|
"request_timeout_ms": 15000,
|
||||||
"max_idle_connections_per_host": 8,
|
"max_idle_connections_per_host": 8,
|
||||||
@@ -308,9 +470,18 @@
|
|||||||
"name": "solana_mainnet_public_ws",
|
"name": "solana_mainnet_public_ws",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"provider": "solana-public",
|
"provider": "solana-public",
|
||||||
"cluster": "mainnet-beta",
|
"cluster": "mainnet",
|
||||||
"kind": "solana_standard",
|
"kind": "solana_standard",
|
||||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}"
|
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}",
|
||||||
|
"capabilities": [
|
||||||
|
"account",
|
||||||
|
"block",
|
||||||
|
"logs",
|
||||||
|
"program",
|
||||||
|
"root",
|
||||||
|
"signature",
|
||||||
|
"slot"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grpc_endpoints": [
|
"grpc_endpoints": [
|
||||||
@@ -318,7 +489,7 @@
|
|||||||
"name": "publicnode_solana_mainnet_yellowstone",
|
"name": "publicnode_solana_mainnet_yellowstone",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"provider": "publicnode",
|
"provider": "publicnode",
|
||||||
"cluster": "mainnet-beta",
|
"cluster": "mainnet",
|
||||||
"protocol": "solana_yellowstone",
|
"protocol": "solana_yellowstone",
|
||||||
"url": "https://solana-yellowstone-grpc.publicnode.com:443",
|
"url": "https://solana-yellowstone-grpc.publicnode.com:443",
|
||||||
"secret_metadata": [
|
"secret_metadata": [
|
||||||
@@ -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",
|
"profile_id": "publicnode_testnet",
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
@@ -367,7 +588,16 @@
|
|||||||
"provider": "solana-public",
|
"provider": "solana-public",
|
||||||
"cluster": "testnet",
|
"cluster": "testnet",
|
||||||
"kind": "solana_standard",
|
"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": [
|
"grpc_endpoints": [
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
],
|
],
|
||||||
"resources": {
|
"resources": {
|
||||||
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
|
"../../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-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-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/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
|
// file: crates/ksp-app-backfill-desk/tests/desktop_contract.rs
|
||||||
// version: 17
|
// version: 18
|
||||||
|
|
||||||
//! Structural desktop contract checks for the Backfill Desk scaffold.
|
//! 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::Some(value) => value,
|
||||||
std::option::Option::None => return,
|
std::option::Option::None => return,
|
||||||
};
|
};
|
||||||
assert_eq!(resources.len(), 15);
|
assert_eq!(resources.len(), 16);
|
||||||
for required in [
|
for required in [
|
||||||
"../../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-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/unit_tests/backfill_request.rs
|
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_request.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
fn ready_options() -> crate::BackfillDeskOptionsDto {
|
fn ready_options() -> crate::BackfillDeskOptionsDto {
|
||||||
let limits = crate::backfill_request_limits();
|
let limits = crate::backfill_request_limits();
|
||||||
@@ -22,7 +22,7 @@ fn ready_options() -> crate::BackfillDeskOptionsDto {
|
|||||||
return crate::BackfillDeskOptionsDto {
|
return crate::BackfillDeskOptionsDto {
|
||||||
commitments: crate::backfill_commitment_codes(),
|
commitments: crate::backfill_commitment_codes(),
|
||||||
composition_ready: true,
|
composition_ready: true,
|
||||||
configured_networks: vec!["mainnet-beta".to_owned()],
|
configured_networks: vec!["mainnet".to_owned()],
|
||||||
http_routes: vec![
|
http_routes: vec![
|
||||||
crate::BackfillHttpRouteOptionDto {
|
crate::BackfillHttpRouteOptionDto {
|
||||||
pooled: true,
|
pooled: true,
|
||||||
@@ -36,7 +36,7 @@ fn ready_options() -> crate::BackfillDeskOptionsDto {
|
|||||||
program_id_options: crate::program_id_autocomplete_options(),
|
program_id_options: crate::program_id_autocomplete_options(),
|
||||||
scope_kinds: crate::backfill_scope_kind_codes(),
|
scope_kinds: crate::backfill_scope_kind_codes(),
|
||||||
store_diagnostic: std::option::Option::None,
|
store_diagnostic: std::option::Option::None,
|
||||||
store_network: std::option::Option::Some("mainnet-beta".to_owned()),
|
store_network: std::option::Option::Some("mainnet".to_owned()),
|
||||||
store_ready: true,
|
store_ready: true,
|
||||||
transport_diagnostic: std::option::Option::None,
|
transport_diagnostic: std::option::Option::None,
|
||||||
transport_ready: true,
|
transport_ready: true,
|
||||||
@@ -81,7 +81,7 @@ fn latest_address_maps_to_backend_network_role_commitment_and_bounds() {
|
|||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(_) => return,
|
std::result::Result::Err(_) => return,
|
||||||
};
|
};
|
||||||
assert_eq!(mapped.network().as_str(), "mainnet-beta");
|
assert_eq!(mapped.network().as_str(), "mainnet");
|
||||||
assert_eq!(mapped.role().as_str(), "backfill_pool");
|
assert_eq!(mapped.role().as_str(), "backfill_pool");
|
||||||
assert_eq!(mapped.commitment(), ksp_job_backfill_lib::BackfillCommitment::Finalized);
|
assert_eq!(mapped.commitment(), ksp_job_backfill_lib::BackfillCommitment::Finalized);
|
||||||
assert_eq!(mapped.scope().kind(), ksp_job_backfill_lib::BackfillScopeKind::LatestAddress);
|
assert_eq!(mapped.scope().kind(), ksp_job_backfill_lib::BackfillScopeKind::LatestAddress);
|
||||||
@@ -193,7 +193,7 @@ fn request_preview_never_contains_address_or_signature_values() {
|
|||||||
if let std::result::Result::Ok(serialized) = serialized {
|
if let std::result::Result::Ok(serialized) = serialized {
|
||||||
assert!(!serialized.contains("11111111111111111111111111111111"));
|
assert!(!serialized.contains("11111111111111111111111111111111"));
|
||||||
assert!(!serialized.contains(&"1".repeat(64)));
|
assert!(!serialized.contains(&"1".repeat(64)));
|
||||||
assert!(serialized.contains("mainnet-beta"));
|
assert!(serialized.contains("mainnet"));
|
||||||
assert!(serialized.contains("backfill_pool"));
|
assert!(serialized.contains("backfill_pool"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_run.rs
|
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_run.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn generated_run_ids_are_backend_owned_bounded_and_unique_in_session() {
|
fn generated_run_ids_are_backend_owned_bounded_and_unique_in_session() {
|
||||||
@@ -30,7 +30,7 @@ fn cancellable_runtime(
|
|||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(_) => return std::option::Option::None,
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
};
|
};
|
||||||
let network = match ksp_store_lib::RawNetworkId::new("mainnet-beta") {
|
let network = match ksp_store_lib::RawNetworkId::new("mainnet") {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(_) => return std::option::Option::None,
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_status.rs
|
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_status.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
fn initial_notification() -> std::option::Option<ksp_job_api::JobNotification<ksp_job_backfill_lib::BackfillJobSnapshot>> {
|
fn initial_notification() -> std::option::Option<ksp_job_api::JobNotification<ksp_job_backfill_lib::BackfillJobSnapshot>> {
|
||||||
let signature = match ksp_job_backfill_lib::BackfillSignature::new("1".repeat(ksp_job_backfill_lib::MIN_BACKFILL_SIGNATURE_TEXT_BYTES)) {
|
let signature = match ksp_job_backfill_lib::BackfillSignature::new("1".repeat(ksp_job_backfill_lib::MIN_BACKFILL_SIGNATURE_TEXT_BYTES)) {
|
||||||
@@ -14,7 +14,7 @@ fn initial_notification() -> std::option::Option<ksp_job_api::JobNotification<ks
|
|||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(_) => return std::option::Option::None,
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
};
|
};
|
||||||
let network = match ksp_store_lib::RawNetworkId::new("mainnet-beta") {
|
let network = match ksp_store_lib::RawNetworkId::new("mainnet") {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(_) => return std::option::Option::None,
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-backfill-desk/unit_tests/store_runtime.rs
|
// file: crates/ksp-app-backfill-desk/unit_tests/store_runtime.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn store_health_mapping_is_explicit_and_conservative() {
|
fn store_health_mapping_is_explicit_and_conservative() {
|
||||||
@@ -10,7 +10,7 @@ fn store_health_mapping_is_explicit_and_conservative() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn store_transport_network_coherence_requires_exact_logical_identity() {
|
fn store_transport_network_coherence_requires_exact_logical_identity() {
|
||||||
assert!(super::validate_network_coherence("devnet", "devnet").is_ok());
|
assert!(super::validate_network_coherence("devnet", "devnet").is_ok());
|
||||||
let mismatch = super::validate_network_coherence("mainnet-beta", "devnet");
|
let mismatch = super::validate_network_coherence("mainnet", "devnet");
|
||||||
assert!(mismatch.is_err());
|
assert!(mismatch.is_err());
|
||||||
if let std::result::Result::Err(error) = mismatch {
|
if let std::result::Result::Err(error) = mismatch {
|
||||||
assert_eq!(error.code(), crate::ERROR_CODE_STORE_NETWORK_MISMATCH);
|
assert_eq!(error.code(), crate::ERROR_CODE_STORE_NETWORK_MISMATCH);
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
],
|
],
|
||||||
"resources": {
|
"resources": {
|
||||||
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
|
"../../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-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-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/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
|
// file: crates/ksp-app-config-desk/tests/desktop_contract.rs
|
||||||
// version: 13
|
// version: 14
|
||||||
|
|
||||||
//! Desktop build/shell contract audits for Config Desk.
|
//! 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);
|
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
|
||||||
assert!(resources.is_some(), "packaged Config resources map must exist");
|
assert!(resources.is_some(), "packaged Config resources map must exist");
|
||||||
if let std::option::Option::Some(resources) = resources {
|
if let std::option::Option::Some(resources) = resources {
|
||||||
assert_eq!(resources.len(), 15);
|
assert_eq!(resources.len(), 16);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resources.get("../../config/composite.ksp-app-backfill-desk.json").and_then(serde_json::Value::as_str),
|
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"),
|
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!(
|
assert_eq!(
|
||||||
resources.get("../../config/composite.ksp-app-solprices-desk.json").and_then(serde_json::Value::as_str),
|
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"),
|
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
|
// file: crates/ksp-app-config-desk/unit_tests/profiles.rs
|
||||||
// version: 8
|
// version: 9
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn profile_inventory_exposes_registered_profile_documents() {
|
fn profile_inventory_exposes_registered_profile_documents() {
|
||||||
@@ -21,6 +21,9 @@ fn profile_inventory_exposes_registered_profile_documents() {
|
|||||||
assert!(inventory.iter().any(|document| -> bool {
|
assert!(inventory.iter().any(|document| -> bool {
|
||||||
return document.file_id == ksp_config_lib::FILE_ID_STD_WALLET;
|
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 {
|
assert!(inventory.iter().any(|document| -> bool {
|
||||||
return document.file_id == ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK;
|
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": {
|
"resources": {
|
||||||
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
|
"../../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-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-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/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
|
// 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`.
|
//! 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]
|
#[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 root = app_root();
|
||||||
let tauri = read_json(root.join("tauri.conf.json").as_path());
|
let tauri = read_json(root.join("tauri.conf.json").as_path());
|
||||||
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
|
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
|
||||||
assert!(resources.is_some());
|
assert!(resources.is_some());
|
||||||
if let std::option::Option::Some(resources) = resources {
|
if let std::option::Option::Some(resources) = resources {
|
||||||
assert_eq!(resources.len(), 15);
|
assert_eq!(resources.len(), 16);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resources.get("../../config/composite.ksp-app-backfill-desk.json").and_then(serde_json::Value::as_str),
|
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"),
|
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!(
|
assert_eq!(
|
||||||
resources.get("../../config/composite.ksp-app-solprices-desk.json").and_then(serde_json::Value::as_str),
|
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"),
|
std::option::Option::Some("config/composite.ksp-app-solprices-desk.json"),
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
],
|
],
|
||||||
"resources": {
|
"resources": {
|
||||||
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
|
"../../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-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-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/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
|
// 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.
|
//! 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();
|
let resources = tauri["bundle"]["resources"].as_object();
|
||||||
assert!(resources.is_some());
|
assert!(resources.is_some());
|
||||||
if let std::option::Option::Some(resources) = resources {
|
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/composite.ksp-app-store-desk.json"));
|
||||||
assert!(resources.contains_key("../../config/std.store.json"));
|
assert!(resources.contains_key("../../config/std.store.json"));
|
||||||
assert!(resources.contains_key("../../config/schemas/std.store.schema.json"));
|
assert!(resources.contains_key("../../config/schemas/std.store.schema.json"));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-store-desk/unit_tests/dto_common.rs
|
// file: crates/ksp-app-store-desk/unit_tests/dto_common.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn command_error_projection_keeps_only_stable_public_fields() {
|
fn command_error_projection_keeps_only_stable_public_fields() {
|
||||||
@@ -19,7 +19,7 @@ fn store_runtime_status_dto_contains_only_safe_portable_fields() {
|
|||||||
diagnostic: std::option::Option::None,
|
diagnostic: std::option::Option::None,
|
||||||
health_state: "ready".to_owned(),
|
health_state: "ready".to_owned(),
|
||||||
migration_version_decimal: std::option::Option::Some("2".to_owned()),
|
migration_version_decimal: std::option::Option::Some("2".to_owned()),
|
||||||
network: std::option::Option::Some("mainnet-beta".to_owned()),
|
network: std::option::Option::Some("mainnet".to_owned()),
|
||||||
pending_migration_count: 0,
|
pending_migration_count: 0,
|
||||||
pool_available: 1,
|
pool_available: 1,
|
||||||
pool_capacity: 8,
|
pool_capacity: 8,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
],
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"tracing:default",
|
"tracing:default"
|
||||||
"dialog:default"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
"@fltsci/tauri-plugin-tracing": "^0.3",
|
"@fltsci/tauri-plugin-tracing": "^0.3",
|
||||||
"@fortawesome/fontawesome-free": "^7.3",
|
"@fortawesome/fontawesome-free": "^7.3",
|
||||||
"@tauri-apps/api": "^2.11",
|
"@tauri-apps/api": "^2.11",
|
||||||
"@tauri-apps/plugin-dialog": "^2.7",
|
|
||||||
"bootstrap": "^5.3",
|
"bootstrap": "^5.3",
|
||||||
"datatables.net-bs5": "^3.0",
|
"datatables.net-bs5": "^3.0",
|
||||||
"datatables.net-select-bs5": "^4.0",
|
"datatables.net-select-bs5": "^4.0",
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
],
|
],
|
||||||
"resources": {
|
"resources": {
|
||||||
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
|
"../../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-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-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/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
|
// 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.
|
//! 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);
|
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");
|
assert!(resources.is_some(), "packaged Wallet Desk Config resources map must exist");
|
||||||
if let std::option::Option::Some(resources) = resources {
|
if let std::option::Option::Some(resources) = resources {
|
||||||
assert_eq!(resources.len(), 15);
|
assert_eq!(resources.len(), 16);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resources.get("../../config/composite.ksp-app-backfill-desk.json").and_then(serde_json::Value::as_str),
|
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"),
|
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!(
|
assert_eq!(
|
||||||
resources.get("../../config/composite.ksp-app-solprices-desk.json").and_then(serde_json::Value::as_str),
|
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"),
|
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
|
// file: crates/ksp-app-wallet-desk/tests/release_compliance.rs
|
||||||
// version: 12
|
// version: 13
|
||||||
|
|
||||||
//! Release-wide deterministic compliance canaries for Wallet Desk.
|
//! 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::Some(value) => value,
|
||||||
std::option::Option::None => return,
|
std::option::Option::None => return,
|
||||||
};
|
};
|
||||||
assert_eq!(resources.len(), 15);
|
assert_eq!(resources.len(), 16);
|
||||||
for (source, destination) in resources {
|
for (source, destination) in resources {
|
||||||
let destination = destination.as_str();
|
let destination = destination.as_str();
|
||||||
assert!(destination.is_some(), "resource destination must be textual");
|
assert!(destination.is_some(), "resource destination must be textual");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-config-lib/README.md -->
|
<!-- file: crates/ksp-config-lib/README.md -->
|
||||||
<!-- version: 12 -->
|
<!-- version: 14 -->
|
||||||
|
|
||||||
# ksp-config-lib
|
# ksp-config-lib
|
||||||
|
|
||||||
@@ -35,8 +35,9 @@ La crate centralise les documents JSON, leurs schemas, les profils et compositio
|
|||||||
Le registre par défaut connaît :
|
Le registre par défaut connaît :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
cfg.composite.ksp-app-backfill-desk -> config/composite.ksp-app-backfill-desk.json
|
cfg.composite.ksp-app-backfill-desk -> config/composite.ksp-app-backfill-desk.json
|
||||||
cfg.composite.ksp-app-solprices-desk -> config/composite.ksp-app-solprices-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-store-desk -> config/composite.ksp-app-store-desk.json
|
||||||
cfg.composite.ksp-app-wallet-desk -> config/composite.ksp-app-wallet-desk.json
|
cfg.composite.ksp-app-wallet-desk -> config/composite.ksp-app-wallet-desk.json
|
||||||
cfg.std.logging -> config/std.logging.json
|
cfg.std.logging -> config/std.logging.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.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/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-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-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-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 ;
|
- [`../../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 -->
|
<!-- file: crates/ksp-config-lib/USAGE.md -->
|
||||||
<!-- version: 14 -->
|
<!-- version: 19 -->
|
||||||
|
|
||||||
# Utilisation de ksp-config-lib
|
# Utilisation de ksp-config-lib
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@ Les arguments compris par Config sont :
|
|||||||
```text
|
```text
|
||||||
--cfgpath=/path/to/config
|
--cfgpath=/path/to/config
|
||||||
--schemapath=/path/to/schemas
|
--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-solprices-desk=my-solprices-desk.json
|
||||||
--filemap=cfg.composite.ksp-app-wallet-desk=my-wallet-desk.json
|
--filemap=cfg.composite.ksp-app-wallet-desk=my-wallet-desk.json
|
||||||
--filemap=cfg.std.logging=my-logging.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`.
|
`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.
|
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.
|
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.
|
||||||
|
|
||||||
@@ -250,7 +253,7 @@ Targets committed :
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
devnet -> network devnet -> KSP_SECRET_STORE_DEVNET_POSTGRES_URI
|
devnet -> network devnet -> KSP_SECRET_STORE_DEVNET_POSTGRES_URI
|
||||||
mainnet -> network mainnet-beta -> KSP_SECRET_STORE_MAINNET_POSTGRES_URI
|
mainnet -> network mainnet -> KSP_SECRET_STORE_MAINNET_POSTGRES_URI
|
||||||
testnet -> network testnet -> KSP_SECRET_STORE_TESTNET_POSTGRES_URI
|
testnet -> network testnet -> KSP_SECRET_STORE_TESTNET_POSTGRES_URI
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -289,7 +292,7 @@ let component = match composite.component("wallet") {
|
|||||||
let wallet = engine.resolve_wallet_config_profile(component.resolved(), &environment);
|
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`
|
## 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.
|
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`
|
## 8. `.env.example`
|
||||||
|
|
||||||
`/.env.example` est l'inventaire versionné. `/.env` reste local et ignoré.
|
`/.env.example` est l'inventaire versionné. `/.env` reste local et ignoré.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/src/lib.rs
|
// file: crates/ksp-config-lib/src/lib.rs
|
||||||
// version: 23
|
// version: 24
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -153,6 +153,8 @@ pub use self::registry::ConfigFileKind;
|
|||||||
pub use self::registry::ConfigFileRegistry;
|
pub use self::registry::ConfigFileRegistry;
|
||||||
/// Default physical filename for the Backfill Desk composite configuration document.
|
/// Default physical filename for the Backfill Desk composite configuration document.
|
||||||
pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_BACKFILL_DESK_FILENAME;
|
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.
|
/// Default physical filename for the SOL Prices Desk composite configuration document.
|
||||||
pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME;
|
pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME;
|
||||||
/// Default physical filename for the Store Desk composite configuration document.
|
/// 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;
|
pub use self::registry::DEFAULT_STD_WALLET_SCHEMA_FILENAME;
|
||||||
/// Logical file identifier for the Backfill Desk composite configuration document.
|
/// Logical file identifier for the Backfill Desk composite configuration document.
|
||||||
pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK;
|
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.
|
/// Logical file identifier for the SOL Prices Desk composite configuration document.
|
||||||
pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK;
|
pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK;
|
||||||
/// Logical file identifier for the Store Desk composite configuration document.
|
/// Logical file identifier for the Store Desk composite configuration document.
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
// file: crates/ksp-config-lib/src/registry.rs
|
// file: crates/ksp-config-lib/src/registry.rs
|
||||||
// version: 15
|
// version: 16
|
||||||
|
|
||||||
/// Bootstrap argument used to replace a known Config filename mapping.
|
/// Bootstrap argument used to replace a known Config filename mapping.
|
||||||
pub const ARG_FILE_MAP: &str = "--filemap";
|
pub const ARG_FILE_MAP: &str = "--filemap";
|
||||||
/// Default physical filename for the Backfill Desk composite configuration document.
|
/// 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";
|
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.
|
/// 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";
|
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.
|
/// 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";
|
pub const DEFAULT_STD_WALLET_SCHEMA_FILENAME: &str = "std.wallet.schema.json";
|
||||||
/// Logical file identifier for the Backfill Desk composite configuration document.
|
/// 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";
|
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.
|
/// 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";
|
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.
|
/// 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::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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(
|
let solprices_composite = ConfigFileDescriptor::new(
|
||||||
FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK,
|
FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK,
|
||||||
ConfigFileKind::Config,
|
ConfigFileKind::Config,
|
||||||
@@ -300,6 +314,7 @@ impl ConfigFileRegistry {
|
|||||||
};
|
};
|
||||||
return crate::build_registry([
|
return crate::build_registry([
|
||||||
backfill_composite,
|
backfill_composite,
|
||||||
|
raw_transaction_ingest_composite,
|
||||||
solprices_composite,
|
solprices_composite,
|
||||||
store_desk_composite,
|
store_desk_composite,
|
||||||
wallet_composite,
|
wallet_composite,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/src/transport.rs
|
// 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.
|
/// 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)]
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
@@ -259,6 +259,8 @@ struct EffectiveWsEndpointSource {
|
|||||||
kind: String,
|
kind: String,
|
||||||
url: String,
|
url: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
capabilities: std::option::Option<std::vec::Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
session: std::option::Option<EffectiveWsSessionOverrideSource>,
|
session: std::option::Option<EffectiveWsSessionOverrideSource>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -812,7 +814,7 @@ fn map_ws_endpoints(
|
|||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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.name,
|
||||||
source.enabled,
|
source.enabled,
|
||||||
ksp_onchain_transport_lib::WsProviderName::new(source.provider),
|
ksp_onchain_transport_lib::WsProviderName::new(source.provider),
|
||||||
@@ -820,7 +822,20 @@ fn map_ws_endpoints(
|
|||||||
protocol,
|
protocol,
|
||||||
url,
|
url,
|
||||||
session,
|
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);
|
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(
|
fn map_ws_session_settings(
|
||||||
defaults: &EffectiveWsSessionSource,
|
defaults: &EffectiveWsSessionSource,
|
||||||
overrides: std::option::Option<&EffectiveWsSessionOverrideSource>,
|
overrides: std::option::Option<&EffectiveWsSessionOverrideSource>,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/tests/ownership.rs
|
// file: crates/ksp-config-lib/tests/ownership.rs
|
||||||
// version: 9
|
// version: 10
|
||||||
|
|
||||||
//! Workspace ownership audits for KSP application configuration boundaries.
|
//! 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 [
|
for token in [
|
||||||
"\".env\"",
|
"\".env\"",
|
||||||
|
"\"composite.ksp-app-raw-transaction-ingest-desk.json\"",
|
||||||
"\"composite.ksp-app-solprices-desk.json\"",
|
"\"composite.ksp-app-solprices-desk.json\"",
|
||||||
"\"composite.ksp-app-wallet-desk.json\"",
|
"\"composite.ksp-app-wallet-desk.json\"",
|
||||||
"\"composite.schema.json\"",
|
"\"composite.schema.json\"",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/tests/public_api.rs
|
// 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,
|
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity,
|
||||||
//! Logging/Transport/Store adapters and management contracts.
|
//! 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:?}");
|
assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}");
|
||||||
if let std::result::Result::Ok(registry) = registry {
|
if let std::result::Result::Ok(registry) = registry {
|
||||||
let descriptors: std::vec::Vec<&ksp_config_lib::ConfigFileDescriptor> = registry.descriptors().collect();
|
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[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[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_STORE_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_WALLET_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_STD_LOGGING);
|
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_OFFCHAIN_TRANSPORT);
|
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_STORE);
|
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_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_WALLET);
|
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_SCHEMA_COMPOSITE);
|
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_STD_LOGGING);
|
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_OFFCHAIN_TRANSPORT);
|
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_STORE);
|
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_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_WALLET);
|
assert_eq!(descriptors[14].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||||
let schema_file_id = descriptors[8].schema_file_id();
|
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");
|
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 {
|
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);
|
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::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::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::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::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");
|
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
|
// 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_FILENAME: &str = "examples/composite.example.json";
|
||||||
const TEST_COMPOSITE_FILE_ID: &str = "cfg.composite.test";
|
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);
|
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> {
|
fn fixture_example_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
|
||||||
let workspace = workspace_root();
|
let workspace = workspace_root();
|
||||||
let fixture_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("unit_tests/fixtures");
|
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"profile_id": "mainnet",
|
"profile_id": "mainnet",
|
||||||
"network": "mainnet-beta",
|
"network": "mainnet",
|
||||||
"backend": "postgres",
|
"backend": "postgres",
|
||||||
"postgres": {
|
"postgres": {
|
||||||
"connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}",
|
"connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}",
|
||||||
|
|||||||
@@ -70,15 +70,33 @@
|
|||||||
"resubscribe": "never",
|
"resubscribe": "never",
|
||||||
"notification_queue_capacity": 48,
|
"notification_queue_capacity": 48,
|
||||||
"max_pending_requests": 24
|
"max_pending_requests": 24
|
||||||
}
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"account",
|
||||||
|
"logs",
|
||||||
|
"program",
|
||||||
|
"root",
|
||||||
|
"signature",
|
||||||
|
"slot"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "fixture_helius_ws",
|
"name": "fixture_helius_ws",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"provider": "helius",
|
"provider": "helius",
|
||||||
"cluster": "mainnet-beta",
|
"cluster": "mainnet",
|
||||||
"kind": "helius_laserstream",
|
"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",
|
"provider": "helius",
|
||||||
"cluster": "devnet",
|
"cluster": "devnet",
|
||||||
"kind": "helius_laserstream",
|
"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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user