harmos-runtime 0.1.6

Harmos runtime: managed state, undo, recovery, background work and typed streams.
Documentation
/* ***
 *
 * Harmos
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * ***
 */

// The Paged Catalog Read: Live Operation Contracts From The Manager, And The
// Schemas, Streams And Objects Storage Holds Durably From Its Saved Frontier.

/* === Modules === */

/* === Re-Exports === */

/* === Imports === */

// Standard Library Imports
use std::num::NonZeroUsize;
use std::sync::Arc;

// Third Party Imports
use serde::Serialize;

// Local Imports
use super::host::{Catalog, CatalogKind, CatalogPage};
use super::{Context, Lane, Operation, OperationManager};
use crate::storage::query::SchemaSelection;
use crate::storage::{IdentityKind, IdentitySelection, ReadLimits, StorageManager, StorageSchema, StoredIdentity};
use crate::{Error, Result};

/* === Constants === */

/* === Macros === */

/* === Type Aliases === */

/* === Structs === */

pub struct Listing<C> {
    storage: Arc<Option<Arc<StorageManager<C>>>>,
}

/* === Enums === */

/* === Traits === */

/* === Implementations === */

impl<C: 'static> Operation for Listing<C> {
    type Declaration = Catalog;
    type Resources = Option<Arc<StorageManager<C>>>;
    type Error = Error;
    type Context<'a> = Context<'a>;
    type Endpoints = ();

    async fn initialize<'ctx>(storage: Arc<Self::Resources>, _: Self::Context<'ctx>) -> Result<Self> {
        Ok(Self { storage })
    }

    async fn run<'ctx>(
        &mut self, context: Self::Context<'ctx>, request: &mut Catalog, _: &mut Self::Endpoints,
    ) -> Result<super::Response<CatalogPage>> {
        let page = match request.kind {
            CatalogKind::Operations => context.operations.discover(&context.source, request)?,
            CatalogKind::Schemas => {
                let schema = request.schema.clone();
                stored::<C, StorageSchema>(self.storage()?, &SchemaSelection { schema: None }, request, |entry| {
                    schema.as_ref().is_none_or(|schema| schema == &entry.key.id)
                })
                .await?
            }
            CatalogKind::Streams | CatalogKind::Objects => {
                let selection = IdentitySelection {
                    kind: if request.kind == CatalogKind::Streams { IdentityKind::Streams } else { IdentityKind::Objects },
                    schema: request.schema.clone(),
                };
                stored::<C, StoredIdentity>(self.storage()?, &selection, request, |_| true).await?
            }
        };
        Ok(page.into())
    }
}

impl<C> Listing<C> {
    fn storage(&self) -> Result<&StorageManager<C>> {
        self.storage.as_deref().ok_or(Error::NotReady { domain: "catalog storage" })
    }
}

/* === Public Functions === */

pub fn register<C: 'static>(
    operations: &OperationManager, owner: impl Into<String>, lane: impl Lane, storage: Option<Arc<StorageManager<C>>>,
) -> Result<()> {
    operations.register_wire_operation::<Listing<C>>(owner.into(), lane, Arc::new(storage))
}

/* === Private Functions === */

// One Page Of Durable Entries After The Request's Storage Position. Pages Order
// By Storage Position, So A Nonempty Page Always Continues And An Empty Page
// Ends The Catalog.
async fn stored<C, T>(
    storage: &StorageManager<C>, selection: &T::Selection, request: &Catalog, keep: impl Fn(&T) -> bool,
) -> Result<CatalogPage>
where
    T: crate::storage::follow::Entry,
{
    let (rows, bytes) = super::manager::limits(request)?;
    let after = request
        .after
        .as_deref()
        .map(str::parse::<u64>)
        .transpose()
        .map_err(|_| Error::InvalidInput { field: "catalog cursor", reason: "cursor must be a storage position" })?
        .unwrap_or(0);
    let limits = ReadLimits {
        rows: NonZeroUsize::new(rows).expect("validated catalog rows"),
        bytes: NonZeroUsize::new(bytes).expect("validated catalog bytes"),
    };
    let page = storage.page::<T>(selection, after, limits).await?;
    let mut entries = vec![b'['];
    let mut next = None;
    let mut count = 0usize;
    for entry in page.entries() {
        let position = entry.position().sequence;
        if keep(entry) {
            let start = entries.len();
            if count > 0 {
                entries.push(b',');
            }
            if write(&mut entries, entry).is_err() || entries.len() + 1 > bytes {
                entries.truncate(start);
                if next.is_none() {
                    return Err(Error::ResourceExhausted { resource: "catalog entry bytes" });
                }
                break;
            }
            count += 1;
        }
        next = Some(position.to_string());
    }
    entries.push(b']');
    Ok(CatalogPage { entries, next })
}

fn write(bytes: &mut Vec<u8>, entry: &impl Serialize) -> std::result::Result<(), serde_json::Error> {
    serde_json::to_writer(bytes, entry)
}

/* === Tests === */