v0.3.2-pre.003

This commit is contained in:
2026-08-29 17:43:52 +02:00
parent ced653bfc0
commit a8c90107b5
13 changed files with 1193 additions and 47 deletions

View File

@@ -0,0 +1,13 @@
// file: crates/ksp-store-lib/src/error.rs
// version: 1
/// Error code reserved for operations attempted after a Store backend has entered its closed state.
pub const ERROR_CODE_BACKEND_CLOSED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "backend_closed");
/// Error code used when a known Store backend was selected but its Cargo feature is not compiled.
pub const ERROR_CODE_BACKEND_NOT_COMPILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "backend_not_compiled");
/// Error code used when a compiled Store backend cannot complete its bounded opening lifecycle.
pub const ERROR_CODE_BACKEND_OPEN_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "backend_open_failed");
/// Error code used when backend-neutral Store settings violate runtime bounds or invariants.
pub const ERROR_CODE_SETTINGS_INVALID: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "settings_invalid");
/// Error code used when a Store cannot complete its explicit shutdown inside the configured bound.
pub const ERROR_CODE_SHUTDOWN_TIMEOUT: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "shutdown_timeout");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/src/lib.rs
// version: 2
// version: 3
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -7,19 +7,170 @@
//! Common backend-neutral Store runtime facade for KSP.
//!
//! `0.3.2-pre.002` establishes only the crate and Cargo feature graph. Runtime
//! settings, backend selection, lifecycle, error mapping and API reexports are
//! introduced by later prereleases of `0.3.2`.
//! `0.3.2-pre.003` materializes Config-independent settings, stable backend
//! identity, bounded validation and the opaque async Store lifecycle contract.
//! Physical PostgreSQL connection, pool, TLS and migrations remain private
//! future slices of this release.
//!
//! The default `postgres` feature compiles the official PostgreSQL backend as
//! an optional implementation dependency. No backend implementation type is
//! part of this crate's public surface.
mod constants;
mod error;
mod settings;
mod store;
/// Crate-owned tracing target reserved for later Store runtime behavior.
/// Error code reserved for operations attempted after a Store backend is closed.
pub use self::error::ERROR_CODE_BACKEND_CLOSED;
/// Error code used when a known Store backend is selected without its compiled feature.
pub use self::error::ERROR_CODE_BACKEND_NOT_COMPILED;
/// Error code used when a compiled Store backend cannot complete opening.
pub use self::error::ERROR_CODE_BACKEND_OPEN_FAILED;
/// Error code used when Store settings violate backend-neutral bounds or invariants.
pub use self::error::ERROR_CODE_SETTINGS_INVALID;
/// Error code used when explicit Store shutdown exceeds its configured deadline.
pub use self::error::ERROR_CODE_SHUTDOWN_TIMEOUT;
/// Bounded PostgreSQL bootstrap and migration settings owned by the Store facade.
pub use self::settings::PostgresBootstrapSettings;
/// Bounded PostgreSQL connection-pool settings owned by the Store facade.
pub use self::settings::PostgresPoolSettings;
/// PostgreSQL settings owned by the Store facade without exposing backend implementation types.
pub use self::settings::PostgresStoreSettings;
/// TLS policy accepted by the backend-neutral PostgreSQL settings surface.
pub use self::settings::PostgresTlsMode;
/// Backend identity understood independently from compiled Cargo features.
pub use self::settings::StoreBackendKind;
/// Backend-specific settings selected through the common Store facade.
pub use self::settings::StoreBackendSettings;
/// Complete backend-neutral settings consumed by the common Store runtime facade.
pub use self::settings::StoreSettings;
/// Opaque common Store runtime facade with consuming async shutdown.
pub use self::store::Store;
/// Error code used when a RAW write collides with divergent content for the same logical identity.
pub use ksp_store_api::ERROR_CODE_RAW_CONFLICT;
/// Error code used when a RAW Store model violates one of its backend-agnostic invariants.
pub use ksp_store_api::ERROR_CODE_RAW_MODEL_INVALID;
/// Error code used when a KSP-owned RAW persistence payload violates its format or admission contract.
pub use ksp_store_api::ERROR_CODE_RAW_PAYLOAD_INVALID;
/// Error code used when acquisition provenance is malformed, unsafe or internally inconsistent.
pub use ksp_store_api::ERROR_CODE_RAW_PROVENANCE_INVALID;
/// Error code used when one RAW query or cursor violates backend-agnostic query invariants.
pub use ksp_store_api::ERROR_CODE_RAW_QUERY_INVALID;
/// Error code used when a RAW retention transition violates the logical lifecycle contract.
pub use ksp_store_api::ERROR_CODE_RAW_RETENTION_INVALID;
/// Common KSP error type used by Store-facing contracts.
pub use ksp_store_api::Error;
/// Stable structured code identifying a KSP error category and condition.
pub use ksp_store_api::ErrorCode;
/// Structured contextual field attached to a KSP error.
pub use ksp_store_api::ErrorContext;
/// Maximum complete RAW account-data length admitted by the Store API.
pub use ksp_store_api::MAX_RAW_ACCOUNT_DATA_BYTES;
/// Maximum UTF-8 byte length accepted for one safe logical RAW/provenance code.
pub use ksp_store_api::MAX_RAW_CODE_BYTES;
/// Maximum opaque query cursor length admitted by the Store API.
pub use ksp_store_api::MAX_RAW_PAGE_CURSOR_BYTES;
/// Maximum KSP-owned canonical RAW payload admitted by the Store API.
pub use ksp_store_api::MAX_RAW_PAYLOAD_BYTES;
/// Maximum source-wire payload size recorded as acquisition metadata.
pub use ksp_store_api::MAX_RAW_SOURCE_PAYLOAD_BYTES;
/// Maximum supported Unix millisecond timestamp.
pub use ksp_store_api::MAX_RAW_UNIX_MILLIS;
/// Canonical Solana account address primitive shared by persistent models.
pub use ksp_store_api::Pubkey;
/// Persistable acquisition observation linked to one complete canonical RAW account state.
pub use ksp_store_api::RawAccountObservation;
/// Read capability for persisted RAW account-state observations.
pub use ksp_store_api::RawAccountObservationRead;
/// Write capability for additional observations of already persisted RAW account states.
pub use ksp_store_api::RawAccountObservationWrite;
/// Canonical complete N1 RAW account state independent from acquisition transport.
pub use ksp_store_api::RawAccountState;
/// Backend-independent list query for complete canonical RAW account states.
pub use ksp_store_api::RawAccountStateQuery;
/// Read capability for complete canonical RAW account states.
pub use ksp_store_api::RawAccountStateRead;
/// Durable backend-independent identity of one canonical RAW account state.
pub use ksp_store_api::RawAccountStateReference;
/// Write capability for complete canonical RAW account-state acquisitions.
pub use ksp_store_api::RawAccountStateWrite;
/// Origin category describing why one acquisition was performed.
pub use ksp_store_api::RawAcquisitionOrigin;
/// Safe source-independent acquisition provenance attached to one persisted observation.
pub use ksp_store_api::RawAcquisitionProvenance;
/// Combined outcome of one atomic canonical RAW entity plus observation acquisition.
pub use ksp_store_api::RawAcquisitionWriteOutcome;
/// Fixed-size digest identifying canonical or source bytes without retaining them.
pub use ksp_store_api::RawContentHash;
/// Outcome for one canonical RAW entity in an idempotent persistence operation.
pub use ksp_store_api::RawEntityWriteOutcome;
/// Bounded identifier of one KSP-owned source-independent RAW persistence format.
pub use ksp_store_api::RawFormatId;
/// Bounded logical network/cluster identifier used in backend-independent Store identities.
pub use ksp_store_api::RawNetworkId;
/// Stable deterministic idempotence key for one persisted acquisition observation.
pub use ksp_store_api::RawObservationKey;
/// Outcome for one deterministic acquisition observation write.
pub use ksp_store_api::RawObservationWriteOutcome;
/// One deterministic page of backend-independent Store results.
pub use ksp_store_api::RawPage;
/// Opaque backend-owned cursor returned by one deterministic Store query.
pub use ksp_store_api::RawPageCursor;
/// Caller-requested page size without an arbitrary KSP policy ceiling.
pub use ksp_store_api::RawPageLimit;
/// Opaque-cursor page request used by backend-independent list operations.
pub use ksp_store_api::RawPageRequest;
/// Bounded source-independent KSP RAW persistence payload.
pub use ksp_store_api::RawPayload;
/// Bounded logical code used by acquisition provenance fields.
pub use ksp_store_api::RawProvenanceCode;
/// Logical availability state of one canonical RAW payload.
pub use ksp_store_api::RawRetentionState;
/// Outcome of one atomic RAW retention transition.
pub use ksp_store_api::RawRetentionWriteOutcome;
/// Optional inclusive Solana slot bounds for one Store query.
pub use ksp_store_api::RawSlotRange;
/// Deterministic traversal direction for Store list queries.
pub use ksp_store_api::RawSortDirection;
/// Bounded UTC timestamp represented as whole milliseconds since Unix epoch.
pub use ksp_store_api::RawTimestamp;
/// Canonical source-independent N1 RAW transaction persisted by Store backends.
pub use ksp_store_api::RawTransaction;
/// Explicit write mode for canonical RAW transaction acquisitions.
pub use ksp_store_api::RawTransactionAcquisitionMode;
/// Persistable acquisition observation linked to one canonical RAW transaction.
pub use ksp_store_api::RawTransactionObservation;
/// Read capability for persisted RAW transaction observations.
pub use ksp_store_api::RawTransactionObservationRead;
/// Write capability for additional observations of already persisted RAW transactions.
pub use ksp_store_api::RawTransactionObservationWrite;
/// Backend-independent list query for canonical RAW transactions.
pub use ksp_store_api::RawTransactionQuery;
/// Read capability for canonical RAW transactions.
pub use ksp_store_api::RawTransactionRead;
/// Durable backend-independent identity of one canonical RAW transaction.
pub use ksp_store_api::RawTransactionReference;
/// Read capability for canonical RAW transaction retention metadata.
pub use ksp_store_api::RawTransactionRetentionRead;
/// Requested compare-and-transition operation for one RAW transaction retention state.
pub use ksp_store_api::RawTransactionRetentionTransition;
/// Write capability for policy-authorized RAW transaction retention transitions.
pub use ksp_store_api::RawTransactionRetentionWrite;
/// Canonical 64-byte Solana transaction signature used by Store identities.
pub use ksp_store_api::RawTransactionSignature;
/// Minimal durable identity retained after a canonical RAW transaction payload is purged.
pub use ksp_store_api::RawTransactionTombstone;
/// Write capability for canonical RAW transaction acquisitions.
pub use ksp_store_api::RawTransactionWrite;
/// Common KSP result alias using [`Error`].
pub use ksp_store_api::Result;
/// Boxed async operation returned by object-safe Store capability contracts.
pub use ksp_store_api::StoreApiFuture;
/// Crate-owned tracing target reserved for Store runtime behavior.
pub(crate) use self::constants::TRACING_TARGET;
// Keep the mandatory crate-owned tracing target part of the compiled scaffold
// without inventing runtime logging before the first behavioral tranche.
const _: &str = TRACING_TARGET;
// without inventing runtime logging before the first behavioral log site.
const _: &str = crate::TRACING_TARGET;

View File

@@ -0,0 +1,381 @@
// file: crates/ksp-store-lib/src/settings.rs
// version: 1
const DEFAULT_CONNECT_TIMEOUT_MS: u64 = 10_000;
const DEFAULT_MAX_CONNECTIONS: u32 = 8;
const DEFAULT_MIGRATION_LOCK_TIMEOUT_MS: u64 = 10_000;
const DEFAULT_MIGRATION_TIMEOUT_MS: u64 = 30_000;
const DEFAULT_POOL_CREATE_TIMEOUT_MS: u64 = 10_000;
const DEFAULT_POOL_RECYCLE_TIMEOUT_MS: u64 = 5_000;
const DEFAULT_POOL_WAIT_TIMEOUT_MS: u64 = 5_000;
const DEFAULT_SHUTDOWN_TIMEOUT_MS: u64 = 5_000;
const MAX_CONNECTIONS: u32 = 64;
const MAX_CONNECT_TIMEOUT_MS: u64 = 60_000;
const MAX_MIGRATION_LOCK_TIMEOUT_MS: u64 = 120_000;
const MAX_MIGRATION_TIMEOUT_MS: u64 = 300_000;
const MAX_POOL_CREATE_TIMEOUT_MS: u64 = 60_000;
const MAX_POOL_RECYCLE_TIMEOUT_MS: u64 = 60_000;
const MAX_POOL_WAIT_TIMEOUT_MS: u64 = 60_000;
const MAX_SHUTDOWN_TIMEOUT_MS: u64 = 30_000;
const MIN_CONNECTIONS: u32 = 1;
const MIN_CONNECT_TIMEOUT_MS: u64 = 100;
const MIN_MIGRATION_LOCK_TIMEOUT_MS: u64 = 100;
const MIN_MIGRATION_TIMEOUT_MS: u64 = 1_000;
const MIN_POOL_CREATE_TIMEOUT_MS: u64 = 100;
const MIN_POOL_RECYCLE_TIMEOUT_MS: u64 = 100;
const MIN_POOL_WAIT_TIMEOUT_MS: u64 = 100;
const MIN_SHUTDOWN_TIMEOUT_MS: u64 = 100;
/// Backend identity understood by the common Store runtime independently from compiled Cargo features.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum StoreBackendKind {
/// Official PostgreSQL Store backend.
Postgres,
}
impl StoreBackendKind {
/// Returns the stable safe backend code used in diagnostics and configuration mapping.
#[must_use]
pub const fn code(&self) -> &'static str {
return match self {
Self::Postgres => "postgres",
};
}
}
/// TLS policy accepted by the backend-neutral PostgreSQL settings surface.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum PostgresTlsMode {
/// Connect without TLS.
Disabled,
/// Require TLS and verify both the certificate chain and requested server identity.
VerifyFull,
}
/// Bounded PostgreSQL connection-pool settings owned by the Store facade.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PostgresPoolSettings {
connect_timeout: std::time::Duration,
create_timeout: std::time::Duration,
max_connections: u32,
recycle_timeout: std::time::Duration,
wait_timeout: std::time::Duration,
}
impl PostgresPoolSettings {
/// Creates explicit PostgreSQL pool bounds without performing any I/O.
#[must_use]
pub const fn new(
max_connections: u32,
connect_timeout: std::time::Duration,
wait_timeout: std::time::Duration,
create_timeout: std::time::Duration,
recycle_timeout: std::time::Duration,
) -> Self {
return Self { connect_timeout, create_timeout, max_connections, recycle_timeout, wait_timeout };
}
/// Returns the timeout for establishing one physical PostgreSQL connection.
#[must_use]
pub const fn connect_timeout(&self) -> std::time::Duration {
return self.connect_timeout;
}
/// Returns the timeout for creating one pooled PostgreSQL object.
#[must_use]
pub const fn create_timeout(&self) -> std::time::Duration {
return self.create_timeout;
}
/// Returns the maximum number of physical PostgreSQL connections owned by the pool.
#[must_use]
pub const fn max_connections(&self) -> u32 {
return self.max_connections;
}
/// Returns the timeout for recycling one pooled PostgreSQL object.
#[must_use]
pub const fn recycle_timeout(&self) -> std::time::Duration {
return self.recycle_timeout;
}
/// Returns the maximum time one acquisition can wait for pool capacity.
#[must_use]
pub const fn wait_timeout(&self) -> std::time::Duration {
return self.wait_timeout;
}
/// Validates all pool bounds without opening a connection.
pub fn validate(&self) -> ksp_store_api::Result<()> {
if self.max_connections < MIN_CONNECTIONS || self.max_connections > MAX_CONNECTIONS {
return std::result::Result::Err(
ksp_store_api::Error::new(crate::ERROR_CODE_SETTINGS_INVALID, "PostgreSQL pool connection bound is invalid")
.with_context("field", "postgres.pool.max_connections")
.with_context("minimum", MIN_CONNECTIONS.to_string())
.with_context("maximum", MAX_CONNECTIONS.to_string()),
);
}
let connect_validation = validate_duration("postgres.pool.connect_timeout", self.connect_timeout, MIN_CONNECT_TIMEOUT_MS, MAX_CONNECT_TIMEOUT_MS);
if let std::result::Result::Err(error) = connect_validation {
return std::result::Result::Err(error);
}
let wait_validation = validate_duration("postgres.pool.wait_timeout", self.wait_timeout, MIN_POOL_WAIT_TIMEOUT_MS, MAX_POOL_WAIT_TIMEOUT_MS);
if let std::result::Result::Err(error) = wait_validation {
return std::result::Result::Err(error);
}
let create_validation = validate_duration("postgres.pool.create_timeout", self.create_timeout, MIN_POOL_CREATE_TIMEOUT_MS, MAX_POOL_CREATE_TIMEOUT_MS);
if let std::result::Result::Err(error) = create_validation {
return std::result::Result::Err(error);
}
let recycle_validation =
validate_duration("postgres.pool.recycle_timeout", self.recycle_timeout, MIN_POOL_RECYCLE_TIMEOUT_MS, MAX_POOL_RECYCLE_TIMEOUT_MS);
if let std::result::Result::Err(error) = recycle_validation {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(());
}
}
impl std::default::Default for PostgresPoolSettings {
fn default() -> Self {
return Self::new(
DEFAULT_MAX_CONNECTIONS,
std::time::Duration::from_millis(DEFAULT_CONNECT_TIMEOUT_MS),
std::time::Duration::from_millis(DEFAULT_POOL_WAIT_TIMEOUT_MS),
std::time::Duration::from_millis(DEFAULT_POOL_CREATE_TIMEOUT_MS),
std::time::Duration::from_millis(DEFAULT_POOL_RECYCLE_TIMEOUT_MS),
);
}
}
/// Bounded PostgreSQL bootstrap settings owned by the Store facade.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PostgresBootstrapSettings {
auto_migrate: bool,
migration_lock_timeout: std::time::Duration,
migration_timeout: std::time::Duration,
}
impl PostgresBootstrapSettings {
/// Creates explicit bootstrap behavior and migration deadlines.
#[must_use]
pub const fn new(auto_migrate: bool, migration_timeout: std::time::Duration, migration_lock_timeout: std::time::Duration) -> Self {
return Self { auto_migrate, migration_lock_timeout, migration_timeout };
}
/// Returns whether pending KSP-owned migrations may be applied during Store opening.
#[must_use]
pub const fn auto_migrate(&self) -> bool {
return self.auto_migrate;
}
/// Returns the bounded wait allowed for the private PostgreSQL migration lock.
#[must_use]
pub const fn migration_lock_timeout(&self) -> std::time::Duration {
return self.migration_lock_timeout;
}
/// Returns the bounded duration allowed for one migration/bootstrap run.
#[must_use]
pub const fn migration_timeout(&self) -> std::time::Duration {
return self.migration_timeout;
}
/// Validates bootstrap and migration deadlines without contacting PostgreSQL.
pub fn validate(&self) -> ksp_store_api::Result<()> {
let migration_validation =
validate_duration("postgres.bootstrap.migration_timeout", self.migration_timeout, MIN_MIGRATION_TIMEOUT_MS, MAX_MIGRATION_TIMEOUT_MS);
if let std::result::Result::Err(error) = migration_validation {
return std::result::Result::Err(error);
}
let lock_validation = validate_duration(
"postgres.bootstrap.migration_lock_timeout",
self.migration_lock_timeout,
MIN_MIGRATION_LOCK_TIMEOUT_MS,
MAX_MIGRATION_LOCK_TIMEOUT_MS,
);
if let std::result::Result::Err(error) = lock_validation {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(());
}
}
impl std::default::Default for PostgresBootstrapSettings {
fn default() -> Self {
return Self::new(
true,
std::time::Duration::from_millis(DEFAULT_MIGRATION_TIMEOUT_MS),
std::time::Duration::from_millis(DEFAULT_MIGRATION_LOCK_TIMEOUT_MS),
);
}
}
/// PostgreSQL settings owned by the Store facade and independent from Config or backend implementation types.
pub struct PostgresStoreSettings {
bootstrap: PostgresBootstrapSettings,
connection_uri: std::string::String,
pool: PostgresPoolSettings,
tls_mode: PostgresTlsMode,
}
impl PostgresStoreSettings {
/// Creates PostgreSQL Store settings from an explicitly supplied sensitive connection URI and typed runtime bounds.
#[must_use]
pub fn new(
connection_uri: impl std::convert::Into<std::string::String>,
pool: PostgresPoolSettings,
tls_mode: PostgresTlsMode,
bootstrap: PostgresBootstrapSettings,
) -> Self {
return Self { bootstrap, connection_uri: connection_uri.into(), pool, tls_mode };
}
/// Returns the PostgreSQL bootstrap settings without exposing the sensitive connection URI.
#[must_use]
pub const fn bootstrap(&self) -> PostgresBootstrapSettings {
return self.bootstrap;
}
/// Returns the PostgreSQL pool settings without exposing the sensitive connection URI.
#[must_use]
pub const fn pool(&self) -> PostgresPoolSettings {
return self.pool;
}
/// Returns the selected PostgreSQL TLS policy without exposing the sensitive connection URI.
#[must_use]
pub const fn tls_mode(&self) -> PostgresTlsMode {
return self.tls_mode;
}
/// Validates backend-neutral PostgreSQL settings without parsing the URI or performing I/O.
pub fn validate(&self) -> ksp_store_api::Result<()> {
if self.connection_uri.is_empty() {
return std::result::Result::Err(
ksp_store_api::Error::new(crate::ERROR_CODE_SETTINGS_INVALID, "PostgreSQL connection URI is required")
.with_context("field", "postgres.connection_uri"),
);
}
let pool_validation = self.pool.validate();
if let std::result::Result::Err(error) = pool_validation {
return std::result::Result::Err(error);
}
let bootstrap_validation = self.bootstrap.validate();
if let std::result::Result::Err(error) = bootstrap_validation {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(());
}
}
impl std::fmt::Debug for PostgresStoreSettings {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("PostgresStoreSettings")
.field("connection_uri", &"<redacted>")
.field("pool", &self.pool)
.field("tls_mode", &self.tls_mode)
.field("bootstrap", &self.bootstrap)
.finish();
}
}
/// Backend-specific settings selected through the common Store facade.
#[derive(Debug)]
#[non_exhaustive]
pub enum StoreBackendSettings {
/// Settings for the known PostgreSQL backend, whether or not its Cargo feature is compiled.
Postgres(PostgresStoreSettings),
}
impl StoreBackendSettings {
/// Returns the stable backend identity represented by these settings.
#[must_use]
pub const fn kind(&self) -> StoreBackendKind {
return match self {
Self::Postgres(_) => StoreBackendKind::Postgres,
};
}
/// Validates backend-specific settings without performing I/O.
pub fn validate(&self) -> ksp_store_api::Result<()> {
return match self {
Self::Postgres(settings) => settings.validate(),
};
}
}
/// Complete backend-neutral settings consumed by the common Store runtime facade.
#[derive(Debug)]
pub struct StoreSettings {
backend: StoreBackendSettings,
shutdown_timeout: std::time::Duration,
}
impl StoreSettings {
/// Creates complete Store runtime settings from one explicit backend and shutdown bound.
#[must_use]
pub const fn new(backend: StoreBackendSettings, shutdown_timeout: std::time::Duration) -> Self {
return Self { backend, shutdown_timeout };
}
/// Returns the selected backend settings.
#[must_use]
pub const fn backend(&self) -> &StoreBackendSettings {
return &self.backend;
}
/// Returns the selected stable backend identity.
#[must_use]
pub const fn backend_kind(&self) -> StoreBackendKind {
return self.backend.kind();
}
/// Returns the maximum duration allowed for explicit Store shutdown.
#[must_use]
pub const fn shutdown_timeout(&self) -> std::time::Duration {
return self.shutdown_timeout;
}
/// Validates all backend-neutral Store settings before any backend I/O can start.
pub fn validate(&self) -> ksp_store_api::Result<()> {
let shutdown_validation = validate_duration("shutdown_timeout", self.shutdown_timeout, MIN_SHUTDOWN_TIMEOUT_MS, MAX_SHUTDOWN_TIMEOUT_MS);
if let std::result::Result::Err(error) = shutdown_validation {
return std::result::Result::Err(error);
}
let backend_validation = self.backend.validate();
if let std::result::Result::Err(error) = backend_validation {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(());
}
}
impl StoreSettings {
/// Creates settings using the common default shutdown bound while keeping backend construction explicit.
#[must_use]
pub fn with_default_shutdown(backend: StoreBackendSettings) -> Self {
return Self::new(backend, std::time::Duration::from_millis(DEFAULT_SHUTDOWN_TIMEOUT_MS));
}
}
fn validate_duration(field: &'static str, value: std::time::Duration, minimum_ms: u64, maximum_ms: u64) -> ksp_store_api::Result<()> {
let minimum = std::time::Duration::from_millis(minimum_ms);
let maximum = std::time::Duration::from_millis(maximum_ms);
if value < minimum || value > maximum {
return std::result::Result::Err(
ksp_store_api::Error::new(crate::ERROR_CODE_SETTINGS_INVALID, "Store runtime duration is outside the supported resource bound")
.with_context("field", field)
.with_context("minimum_ms", minimum_ms.to_string())
.with_context("maximum_ms", maximum_ms.to_string()),
);
}
return std::result::Result::Ok(());
}
#[cfg(test)]
#[path = "../unit_tests/settings.rs"]
mod tests;

View File

@@ -0,0 +1,56 @@
// file: crates/ksp-store-lib/src/store.rs
// version: 1
/// Opaque common Store runtime facade.
///
/// A successful value is returned only after the selected backend has completed its bounded readiness path. `0.3.2-pre.003` establishes the lifecycle
/// contract but intentionally has no successful opening path until the PostgreSQL runtime foundation is materialized by later prereleases.
#[derive(Debug)]
#[non_exhaustive]
pub struct Store;
impl Store {
/// Validates settings, selects the requested backend and opens a ready Store runtime.
///
/// A known backend whose Cargo feature is absent is rejected before any I/O. During `0.3.2-pre.003`, the compiled PostgreSQL path also stops before I/O
/// with [`crate::ERROR_CODE_BACKEND_OPEN_FAILED`] because physical connection ownership is introduced in `pre.005`.
pub async fn open(settings: crate::StoreSettings) -> ksp_store_api::Result<Self> {
let validation = settings.validate();
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
let backend_kind = settings.backend_kind();
return match backend_kind {
crate::StoreBackendKind::Postgres => open_postgres_contract(backend_kind).await,
};
}
/// Explicitly closes the Store runtime and consumes its facade handle.
///
/// `pre.003` cannot yet produce a successful Store instance, so the physical bounded shutdown path remains reserved for backend composition. The consuming
/// async signature is fixed here so no pool or backend handle needs to escape later.
pub async fn close(self) -> ksp_store_api::Result<()> {
return std::result::Result::Ok(());
}
}
#[cfg(feature = "postgres")]
async fn open_postgres_contract(backend_kind: crate::StoreBackendKind) -> ksp_store_api::Result<Store> {
return std::result::Result::Err(
ksp_store_api::Error::new(crate::ERROR_CODE_BACKEND_OPEN_FAILED, "PostgreSQL Store runtime opening is not materialized in this prerelease")
.with_context("backend", backend_kind.code())
.with_context("stage", "runtime_foundation_pending"),
);
}
#[cfg(not(feature = "postgres"))]
async fn open_postgres_contract(backend_kind: crate::StoreBackendKind) -> ksp_store_api::Result<Store> {
return std::result::Result::Err(
ksp_store_api::Error::new(crate::ERROR_CODE_BACKEND_NOT_COMPILED, "Selected Store backend is not compiled")
.with_context("backend", backend_kind.code()),
);
}
#[cfg(test)]
#[path = "../unit_tests/store.rs"]
mod tests;

View File

@@ -1,11 +1,11 @@
// file: crates/ksp-store-lib/tests/dependency_boundary.rs
// version: 2
// version: 3
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Cargo-feature and dependency-boundary canaries for the common Store runtime scaffold.
//! Cargo-feature and dependency-boundary canaries for the common Store runtime facade.
#[test]
fn pre_002_manifest_owns_default_postgres_feature_and_optional_backend_edge() {
@@ -24,30 +24,27 @@ fn pre_002_manifest_owns_default_postgres_feature_and_optional_backend_edge() {
"deadpool-postgres",
"rustls",
] {
assert!(!manifest.contains(forbidden), "forbidden pre.002 Store facade dependency detected: {forbidden}");
assert!(!manifest.contains(forbidden), "forbidden Store facade dependency detected: {forbidden}");
}
return;
}
#[test]
fn pre_002_scaffold_keeps_backend_types_and_future_runtime_surface_private() {
fn pre_003_facade_adds_only_backend_neutral_settings_lifecycle_and_api_reexports() {
let crate_root = include_str!("../src/lib.rs");
assert!(crate_root.contains("mod constants;"));
assert!(crate_root.contains("pub(crate) use self::constants::TRACING_TARGET;"));
assert!(crate_root.contains("const _: &str = TRACING_TARGET;"));
for forbidden in [
"pub mod ",
"pub use ksp_store_postgres_lib",
"StoreSettings",
"StoreBackendSettings",
"PostgresStoreSettings",
"pub struct Store",
"tokio_postgres",
"deadpool_postgres",
] {
assert!(!crate_root.contains(forbidden), "forbidden pre.002 Store facade surface detected: {forbidden}");
assert!(crate_root.contains("mod error;"));
assert!(crate_root.contains("mod settings;"));
assert!(crate_root.contains("mod store;"));
assert!(crate_root.contains("pub use self::settings::StoreSettings;"));
assert!(crate_root.contains("pub use self::store::Store;"));
assert!(crate_root.contains("pub use ksp_store_api::RawTransaction;"));
assert!(crate_root.contains("const _: &str = crate::TRACING_TARGET;"));
for forbidden in ["pub mod ", "pub use ksp_store_postgres_lib", "tokio_postgres", "deadpool_postgres", "rustls::", "Pool", "Client", "Row", "Statement"] {
assert!(!crate_root.contains(forbidden), "forbidden physical backend facade surface detected: {forbidden}");
}
let production = format!("{}\n{}", include_str!("../src/settings.rs"), include_str!("../src/store.rs"));
for forbidden in ["ksp_config_lib", "std::env", "dotenv", "PGHOST", "PGPORT", "PGUSER", "PGPASSWORD", ".pgpass", "tokio_postgres", "deadpool_postgres"] {
assert!(!production.contains(forbidden), "forbidden Store facade ownership bypass detected: {forbidden}");
}
let constants = include_str!("../src/constants.rs");
assert!(constants.contains("pub(crate) const TRACING_TARGET: &str = \"ksp-store-lib\";"));
return;
}

View File

@@ -0,0 +1,46 @@
// file: crates/ksp-store-lib/tests/feature_mismatch.rs
// version: 1
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Feature-selection canaries for known Store backends.
fn poll_ready<T>(future: impl std::future::Future<Output = T>) -> T {
let mut future = std::boxed::Box::pin(future);
let waker = std::task::Waker::noop();
let mut context = std::task::Context::from_waker(waker);
return match std::future::Future::poll(future.as_mut(), &mut context) {
std::task::Poll::Ready(value) => value,
std::task::Poll::Pending => panic!("pre.003 Store feature-dispatch future unexpectedly became pending before any I/O exists"),
};
}
fn settings() -> ksp_store_lib::StoreSettings {
let postgres = ksp_store_lib::PostgresStoreSettings::new(
"postgresql://operator-supplied-sensitive-value",
ksp_store_lib::PostgresPoolSettings::default(),
ksp_store_lib::PostgresTlsMode::Disabled,
ksp_store_lib::PostgresBootstrapSettings::default(),
);
return ksp_store_lib::StoreSettings::with_default_shutdown(ksp_store_lib::StoreBackendSettings::Postgres(postgres));
}
#[cfg(not(feature = "postgres"))]
#[test]
fn pre_003_known_postgres_without_feature_returns_stable_error_before_io() {
let result = poll_ready(ksp_store_lib::Store::open(settings()));
let error = result.err();
assert_eq!(error.map(|value| value.code()), std::option::Option::Some(ksp_store_lib::ERROR_CODE_BACKEND_NOT_COMPILED));
return;
}
#[cfg(feature = "postgres")]
#[test]
fn pre_003_compiled_postgres_path_refuses_to_fake_readiness_before_pre_005() {
let result = poll_ready(ksp_store_lib::Store::open(settings()));
let error = result.err();
assert_eq!(error.map(|value| value.code()), std::option::Option::Some(ksp_store_lib::ERROR_CODE_BACKEND_OPEN_FAILED));
return;
}

View File

@@ -0,0 +1,45 @@
// file: crates/ksp-store-lib/tests/public_api.rs
// version: 1
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Public API canaries for Store settings, lifecycle and Store API reexports.
#[test]
fn pre_003_settings_and_lifecycle_contract_are_available_from_crate_root() {
let postgres = ksp_store_lib::PostgresStoreSettings::new(
"postgresql://operator-supplied-sensitive-value",
ksp_store_lib::PostgresPoolSettings::default(),
ksp_store_lib::PostgresTlsMode::VerifyFull,
ksp_store_lib::PostgresBootstrapSettings::default(),
);
let settings = ksp_store_lib::StoreSettings::with_default_shutdown(ksp_store_lib::StoreBackendSettings::Postgres(postgres));
assert_eq!(settings.backend_kind(), ksp_store_lib::StoreBackendKind::Postgres);
assert!(settings.validate().is_ok());
let _open = ksp_store_lib::Store::open;
let _close = ksp_store_lib::Store::close;
return;
}
#[test]
fn pre_003_common_error_codes_are_stable_and_store_owned() {
assert_eq!(ksp_store_lib::ERROR_CODE_SETTINGS_INVALID.domain(), "store");
assert_eq!(ksp_store_lib::ERROR_CODE_SETTINGS_INVALID.code(), "settings_invalid");
assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_NOT_COMPILED.code(), "backend_not_compiled");
assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_OPEN_FAILED.code(), "backend_open_failed");
assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_CLOSED.code(), "backend_closed");
assert_eq!(ksp_store_lib::ERROR_CODE_SHUTDOWN_TIMEOUT.code(), "shutdown_timeout");
return;
}
#[test]
fn pre_003_facade_reexports_backend_agnostic_store_api_types() {
let _raw_transaction = std::mem::size_of::<std::option::Option<ksp_store_lib::RawTransaction>>();
let _raw_account_state = std::mem::size_of::<std::option::Option<ksp_store_lib::RawAccountState>>();
let _query = std::mem::size_of::<std::option::Option<ksp_store_lib::RawTransactionQuery>>();
let _capability = std::mem::size_of::<std::option::Option<&dyn ksp_store_lib::RawTransactionRead>>();
let _result: ksp_store_lib::Result<()> = std::result::Result::Ok(());
return;
}

View File

@@ -0,0 +1,139 @@
// file: crates/ksp-store-lib/unit_tests/settings.rs
// version: 1
fn valid_postgres_settings() -> crate::PostgresStoreSettings {
return crate::PostgresStoreSettings::new(
"postgresql://secret-user:secret-password@db.internal/ksp",
crate::PostgresPoolSettings::default(),
crate::PostgresTlsMode::VerifyFull,
crate::PostgresBootstrapSettings::default(),
);
}
#[test]
fn defaults_match_the_pre_001_runtime_bounds() {
let pool = crate::PostgresPoolSettings::default();
assert_eq!(pool.max_connections(), 8);
assert_eq!(pool.connect_timeout(), std::time::Duration::from_millis(10_000));
assert_eq!(pool.wait_timeout(), std::time::Duration::from_millis(5_000));
assert_eq!(pool.create_timeout(), std::time::Duration::from_millis(10_000));
assert_eq!(pool.recycle_timeout(), std::time::Duration::from_millis(5_000));
let bootstrap = crate::PostgresBootstrapSettings::default();
assert!(bootstrap.auto_migrate());
assert_eq!(bootstrap.migration_timeout(), std::time::Duration::from_millis(30_000));
assert_eq!(bootstrap.migration_lock_timeout(), std::time::Duration::from_millis(10_000));
let store = crate::StoreSettings::with_default_shutdown(crate::StoreBackendSettings::Postgres(valid_postgres_settings()));
assert_eq!(store.shutdown_timeout(), std::time::Duration::from_millis(5_000));
assert_eq!(store.backend_kind(), crate::StoreBackendKind::Postgres);
return;
}
#[test]
fn exact_runtime_boundaries_validate_and_adjacent_values_are_rejected() {
let minimum_pool = crate::PostgresPoolSettings::new(
1,
std::time::Duration::from_millis(100),
std::time::Duration::from_millis(100),
std::time::Duration::from_millis(100),
std::time::Duration::from_millis(100),
);
assert!(minimum_pool.validate().is_ok());
let maximum_pool = crate::PostgresPoolSettings::new(
64,
std::time::Duration::from_millis(60_000),
std::time::Duration::from_millis(60_000),
std::time::Duration::from_millis(60_000),
std::time::Duration::from_millis(60_000),
);
assert!(maximum_pool.validate().is_ok());
assert!(
crate::PostgresPoolSettings::new(
0,
std::time::Duration::from_millis(100),
std::time::Duration::from_millis(100),
std::time::Duration::from_millis(100),
std::time::Duration::from_millis(100),
)
.validate()
.is_err()
);
assert!(
crate::PostgresPoolSettings::new(
65,
std::time::Duration::from_millis(100),
std::time::Duration::from_millis(100),
std::time::Duration::from_millis(100),
std::time::Duration::from_millis(100),
)
.validate()
.is_err()
);
assert!(
crate::PostgresPoolSettings::new(
1,
std::time::Duration::from_millis(99),
std::time::Duration::from_millis(100),
std::time::Duration::from_millis(100),
std::time::Duration::from_millis(100),
)
.validate()
.is_err()
);
assert!(
crate::PostgresPoolSettings::new(
1,
std::time::Duration::from_millis(100),
std::time::Duration::from_millis(60_001),
std::time::Duration::from_millis(100),
std::time::Duration::from_millis(100),
)
.validate()
.is_err()
);
let minimum_bootstrap = crate::PostgresBootstrapSettings::new(false, std::time::Duration::from_millis(1_000), std::time::Duration::from_millis(100));
assert!(minimum_bootstrap.validate().is_ok());
let maximum_bootstrap = crate::PostgresBootstrapSettings::new(true, std::time::Duration::from_millis(300_000), std::time::Duration::from_millis(120_000));
assert!(maximum_bootstrap.validate().is_ok());
assert!(
crate::PostgresBootstrapSettings::new(true, std::time::Duration::from_millis(999), std::time::Duration::from_millis(100),)
.validate()
.is_err()
);
assert!(
crate::PostgresBootstrapSettings::new(true, std::time::Duration::from_millis(1_000), std::time::Duration::from_millis(120_001),)
.validate()
.is_err()
);
return;
}
#[test]
fn store_shutdown_bound_is_independent_from_backend_and_rejects_outside_values() {
let valid = crate::StoreSettings::new(crate::StoreBackendSettings::Postgres(valid_postgres_settings()), std::time::Duration::from_millis(100));
assert!(valid.validate().is_ok());
let invalid = crate::StoreSettings::new(crate::StoreBackendSettings::Postgres(valid_postgres_settings()), std::time::Duration::from_millis(30_001));
let error = invalid.validate().err();
assert_eq!(error.map(|value| value.code()), std::option::Option::Some(crate::ERROR_CODE_SETTINGS_INVALID));
return;
}
#[test]
fn connection_uri_is_required_but_never_rendered_by_debug_or_validation_error() {
let secret = "postgresql://secret-user:secret-password@db.internal/ksp";
let settings = valid_postgres_settings();
let debug = format!("{settings:?}");
assert!(!debug.contains(secret));
assert!(!debug.contains("secret-user"));
assert!(!debug.contains("secret-password"));
assert!(debug.contains("<redacted>"));
let empty = crate::PostgresStoreSettings::new(
std::string::String::new(),
crate::PostgresPoolSettings::default(),
crate::PostgresTlsMode::Disabled,
crate::PostgresBootstrapSettings::default(),
);
let error = empty.validate().err();
assert_eq!(error.as_ref().map(|value| value.code()), std::option::Option::Some(crate::ERROR_CODE_SETTINGS_INVALID));
assert!(!format!("{:?}", error).contains(secret));
return;
}

View File

@@ -0,0 +1,55 @@
// file: crates/ksp-store-lib/unit_tests/store.rs
// version: 1
fn poll_ready<T>(future: impl std::future::Future<Output = T>) -> T {
let mut future = std::boxed::Box::pin(future);
let waker = std::task::Waker::noop();
let mut context = std::task::Context::from_waker(waker);
return match std::future::Future::poll(future.as_mut(), &mut context) {
std::task::Poll::Ready(value) => value,
std::task::Poll::Pending => panic!("pre.003 Store contract future unexpectedly became pending before any I/O exists"),
};
}
fn valid_store_settings() -> crate::StoreSettings {
let postgres = crate::PostgresStoreSettings::new(
"postgresql://secret-user:secret-password@db.internal/ksp",
crate::PostgresPoolSettings::default(),
crate::PostgresTlsMode::VerifyFull,
crate::PostgresBootstrapSettings::default(),
);
return crate::StoreSettings::with_default_shutdown(crate::StoreBackendSettings::Postgres(postgres));
}
#[test]
fn invalid_settings_are_rejected_before_backend_dispatch() {
let postgres = crate::PostgresStoreSettings::new(
std::string::String::new(),
crate::PostgresPoolSettings::default(),
crate::PostgresTlsMode::Disabled,
crate::PostgresBootstrapSettings::default(),
);
let settings = crate::StoreSettings::with_default_shutdown(crate::StoreBackendSettings::Postgres(postgres));
let result = poll_ready(crate::Store::open(settings));
let error = result.err();
assert_eq!(error.map(|value| value.code()), std::option::Option::Some(crate::ERROR_CODE_SETTINGS_INVALID));
return;
}
#[cfg(feature = "postgres")]
#[test]
fn compiled_postgres_dispatch_does_not_fake_readiness_before_connection_materialization() {
let result = poll_ready(crate::Store::open(valid_store_settings()));
let error = result.err();
assert_eq!(error.map(|value| value.code()), std::option::Option::Some(crate::ERROR_CODE_BACKEND_OPEN_FAILED));
return;
}
#[cfg(not(feature = "postgres"))]
#[test]
fn known_postgres_without_feature_is_rejected_before_io() {
let result = poll_ready(crate::Store::open(valid_store_settings()));
let error = result.err();
assert_eq!(error.map(|value| value.code()), std::option::Option::Some(crate::ERROR_CODE_BACKEND_NOT_COMPILED));
return;
}