islands-core 0.1.2

Server-side SSR primitives for islands.rs: island markers, the page shell, the asset manifest, and streaming Suspense.
Documentation
use std::future::Future;
use std::pin::Pin;

use bytes::Bytes;

use crate::stream::{with_active, PendingSuspense, StreamError};

/// Inline JS written into every page's `<head>` once via `page_shell`.
///
/// Defines `window.$ISLANDS_REPLACE(slotId, tplId)` — called by deferred Suspense chunks
/// to swap a fallback placeholder with the resolved content. The optional-call on
/// `__islands_remount` handles the race where WASM hasn't initialised yet; the eventual
/// `mount_all()` will pick up newly-arrived island markers on its own sweep.
pub const REPLACE_SCRIPT: &str = r#"window.$ISLANDS_REPLACE=(s,t)=>{const o=document.getElementById(s),p=document.getElementById(t);if(o&&p){o.replaceChildren(p.content);p.remove();window.__islands_remount?.();}};"#;

/// A streaming Suspense boundary consumable from rsx-rendered HTML.
///
/// `Suspense::new(fallback, future).render()` emits the same
/// `<div data-suspense-slot id="S:N">{fallback}</div>` placeholder that
/// `HtmlChunk::Suspense` would emit AND registers the resolution future with
/// the active `RenderStreamContext`. The future is later drained by
/// `render_streaming_with` and flushed as a `<template id="T:N">{body}</template>`
/// chunk followed by the `$ISLANDS_REPLACE` script.
///
/// The slot ID is auto-assigned from a per-render counter — declaration order
/// matches assignment order (first `<Suspense>` rendered gets `S:1`, second
/// `S:2`, etc.). Resolution order is independent and is driven by future
/// completion order in `FuturesUnordered`.
///
/// Calling `render` outside `render_streaming_with` panics with the message
/// `RenderStreamContext not active`.
pub struct Suspense<F> {
    fallback: Bytes,
    future: F,
}

impl<F> Suspense<F>
where
    F: Future<Output = Result<String, StreamError>> + Send + 'static,
{
    pub fn new(fallback: impl Into<Bytes>, future: F) -> Self {
        Self {
            fallback: fallback.into(),
            future,
        }
    }

    /// Allocate a slot ID, register the future on the active stream context,
    /// and return the placeholder HTML for the Suspense boundary.
    ///
    /// The returned `String` can be spliced directly into a larger HTML
    /// buffer (e.g. inside a `{...}` block within `rsx!`).
    pub fn render(self) -> String {
        let Self { fallback, future } = self;
        let slot_id = with_active(|context| {
            let slot_id = context.next_slot_id();
            context.register(PendingSuspense {
                slot_id,
                fallback_html: fallback.clone(),
                future: Box::pin(future) as Pin<Box<dyn Future<Output = _> + Send>>,
            });
            slot_id
        });
        format!(
            "<div data-suspense-slot id=\"S:{slot_id}\">{}</div>",
            std::str::from_utf8(&fallback).unwrap_or("")
        )
    }
}