use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::task::{Context, Poll};
use alkcall::channels::operations::{
Establishment, EstablishmentError, OpenEstablisher, PubOpenHandler,
};
use alkcall::core::types::Connection;
use alkcall::protocol::wire::CallError;
use alkcall::registry::spec::{
AccessControl, ChannelOpenSpec, OperationSpec, OperationType, Visibility,
};
use serde_json::{json, Value};
use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf};
use tokio::sync::mpsc;
use super::fetch::{FanoutPublisher, SlowSubscriberPolicy};
use super::tokens::OpsContext;
use super::wire::{
declared_error, digest_ingress, digest_mismatch_error, digest_wire_schema, mint_refused_error,
namespace_schema, put_message_details_schema, put_refused_error, BLOBS_CHANNEL_ALPN,
DIGEST_MISMATCH, DIGEST_WIRE_PATTERN, MINT_REFUSED, PUT_OP_ID, PUT_REFUSED,
VERIFICATION_REFUSED,
};
use crate::facade::Pin;
use crate::hashing::BlobHasher;
use crate::keys::Key;
use crate::Error;
const PUT_WIRE_CHUNK: usize = 16 * 1024;
const PUT_PIPE_BOUND: usize = 64;
const PUT_ACK_FIELD: &str = "accepted";
pub fn put_openables(ctx: Arc<OpsContext>) -> (OperationSpec, OpenEstablisher, PubOpenHandler) {
let spec = OperationSpec::new(
PUT_OP_ID,
OperationType::Pub,
Visibility::External,
json!({
"type": "object",
"properties": {
"namespace": namespace_schema(),
"key": digest_wire_schema(),
"len": { "type": "integer", "minimum": 0 }
},
"required": ["namespace", "key", "len"],
"additionalProperties": false
}),
json!({
"type": "object",
"properties": {
"channel_id": { "type": "integer", "minimum": 1 },
PUT_ACK_FIELD: { "type": "boolean" },
"token": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
"key": { "type": "string", "pattern": DIGEST_WIRE_PATTERN }
},
"required": ["channel_id", "token", "key"],
"additionalProperties": false
}),
vec![
declared_error(
DIGEST_MISMATCH,
"the offered key is not the stream's derivation under the key's own \
algorithm (a claim contradicted by the authority): nothing handed over",
put_message_details_schema(),
),
declared_error(
VERIFICATION_REFUSED,
"the stream failed verification against the offered key's own derivation \
(length or byte mismatch): nothing committed, zero residue on both tiers",
put_message_details_schema(),
),
declared_error(
PUT_REFUSED,
"the put refused before or during the stream (the kv-only over-threshold \
rejection, ADR-012 §6.2, and other put-time store failures): nothing committed",
put_message_details_schema(),
),
declared_error(
MINT_REFUSED,
"the hand-over's mint was refused at the token registry's admission cap \
(ADR-019): the put's stream already committed and verified on this node, \
but no token was issued and no pin custody was taken — non-fleet the \
entry stands as unreferenced content a sweep may reclaim, fleet the pin \
row stands and expires by its TTL; the re-put is CAS-idempotent",
put_message_details_schema(),
),
],
AccessControl {
resource_type: Some("blob-namespace".to_owned()),
resource_action: Some("write".to_owned()),
..AccessControl::default()
},
Some("$.namespace".to_owned()),
)
.with_channel_open(ChannelOpenSpec::new(BLOBS_CHANNEL_ALPN))
.with_description(
"the verified put: {namespace, key, len} offer + the byte stream over the \
alk/blobs channel; server verifies against the offered key's derivation before \
commit; the single response merges the offer ack and {token, key}",
);
let establisher: OpenEstablisher =
{ Arc::new(move |input, _auth| Box::pin(async move { put_establish(input).await })) };
let handler: PubOpenHandler = Arc::new(move |input, _plan, channel_conn, _auth| {
let ctx = Arc::clone(&ctx);
Box::pin(async move { put_handle(ctx, input, channel_conn).await })
});
(spec, establisher, handler)
}
async fn put_establish(input: Value) -> Result<Establishment, EstablishmentError> {
let Some(key_hex) = input.get("key").and_then(Value::as_str) else {
return Err(EstablishmentError::HandlerError {
message: "the key field is missing after the schema gate (unreachable \
post-validation)"
.to_owned(),
});
};
let _ = digest_ingress(key_hex).map_err(|_| EstablishmentError::HandlerError {
message: format!("the key failed its ingress: {key_hex}"),
})?;
Ok(Establishment::default().with_reply_field(PUT_ACK_FIELD, json!(true)))
}
struct PutPipeWriter {
tx: Option<mpsc::Sender<Arc<Vec<u8>>>>,
fail: Arc<AtomicBool>,
}
impl PutPipeWriter {
async fn feed(&mut self, chunk: Arc<Vec<u8>>) -> bool {
let Some(tx) = &self.tx else {
return false;
};
tx.send(chunk).await.is_ok()
}
fn eof(&mut self) {
self.tx = None;
}
fn kill(&mut self) {
self.fail.store(true, Ordering::Release);
self.tx = None;
}
}
struct PutPipeReader {
rx: mpsc::Receiver<Arc<Vec<u8>>>,
fail: Arc<AtomicBool>,
buffer: Option<Arc<Vec<u8>>>,
offset: usize,
}
impl AsyncRead for PutPipeReader {
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<tokio::io::Result<()>> {
let this = self.get_mut();
loop {
if this.fail.load(Ordering::Acquire) {
return Poll::Ready(Err(tokio::io::Error::new(
tokio::io::ErrorKind::BrokenPipe,
"the put's feed failed pre-commit (the stream errored)",
)));
}
if let Some(chunk) = &this.buffer {
if this.offset < chunk.len() {
let src = &chunk[this.offset..];
let n = src.len().min(buf.remaining());
buf.put_slice(&src[..n]);
this.offset += n;
return Poll::Ready(Ok(()));
}
this.buffer = None;
this.offset = 0;
}
match this.rx.poll_recv(cx) {
Poll::Ready(Some(chunk)) => {
this.buffer = Some(chunk);
this.offset = 0;
}
Poll::Ready(None) => return Poll::Ready(Ok(())),
Poll::Pending => return Poll::Pending,
}
}
}
}
enum PutStreamEnd {
Eof,
WireFailed,
PutEnded,
}
async fn put_handle(
ctx: Arc<OpsContext>,
input: Value,
channel_conn: Connection,
) -> Result<Value, CallError> {
let namespace = match input.get("namespace").and_then(Value::as_str) {
Some(namespace) => namespace.to_owned(),
None => return Err(CallError::invalid_input(
"the namespace field is missing after the schema gate (unreachable post-validation)",
)),
};
let Some(key_hex) = input.get("key").and_then(Value::as_str) else {
return Err(CallError::invalid_input(
"the key field is missing after the schema gate (unreachable post-validation)",
));
};
let offered_key = digest_ingress(key_hex)?;
let Some(offered_len) = input.get("len").and_then(Value::as_u64) else {
return Err(CallError::invalid_input(
"the len field is missing after the schema gate (unreachable post-validation)",
));
};
let offered_hex = hex::encode(offered_key.as_bytes());
let publisher = ctx
.fanout()
.begin(&offered_key, SlowSubscriberPolicy::Stall);
let mut bidi = channel_conn
.accept_bi()
.await
.map_err(|_| CallError::internal("the put's data plane never opened"))?;
let (tx, rx) = mpsc::channel(PUT_PIPE_BOUND);
let fail = Arc::new(AtomicBool::new(false));
let put_task = {
let store = Arc::clone(ctx.store());
let reader_fail = Arc::clone(&fail);
let value = offered_key.clone();
tokio::spawn(async move {
let reader = PutPipeReader {
rx,
fail: reader_fail,
buffer: None,
offset: 0,
};
store
.put_as(value.algorithm(), Some(offered_len), reader)
.await
})
};
let mut writer = PutPipeWriter {
tx: Some(tx),
fail: Arc::clone(&fail),
};
let mut hasher = BlobHasher::new(offered_key.algorithm(), offered_len);
let mut read_buf = vec![0u8; PUT_WIRE_CHUNK];
let stream_end = loop {
if put_task.is_finished() {
break PutStreamEnd::PutEnded;
}
match bidi.read(&mut read_buf).await {
Ok(0) => {
break PutStreamEnd::Eof;
}
Ok(n) => {
let chunk: Arc<Vec<u8>> = Arc::new(read_buf[..n].to_vec());
hasher.update(&chunk[..]);
if let Some(publisher) = &publisher {
publisher.broadcast(Arc::clone(&chunk)).await;
}
if !writer.feed(Arc::clone(&chunk)).await {
break PutStreamEnd::PutEnded;
}
}
Err(_) => {
break PutStreamEnd::WireFailed;
}
}
};
match stream_end {
PutStreamEnd::Eof => {
let derived = Key::from(hasher.finish());
if derived != offered_key {
writer.kill();
drop(put_task.await);
return Err(digest_mismatch_error(
&offered_hex,
&hex::encode(derived.as_bytes()),
));
}
writer.eof();
finish_put(ctx, &namespace, put_task, publisher).await
}
PutStreamEnd::WireFailed | PutStreamEnd::PutEnded => {
writer.kill();
put_failed_response(put_task, Some(&offered_hex)).await
}
}
}
async fn put_failed_response(
put_task: tokio::task::JoinHandle<Result<(Key, Pin), Error>>,
offered_hex: Option<&str>,
) -> Result<Value, CallError> {
match put_task.await {
Ok(Err(error)) => Err(put_refused_error("the store arm", offered_hex, error)),
Ok(Ok(..)) => Err(CallError::internal(
"the put's store arm completed against a torn stream; ADR-005's CAS re-put is \
the backstop",
)),
Err(join_error) => Err(CallError::internal(format!(
"the put's store task failed ({})",
if join_error.is_cancelled() {
"cancelled"
} else {
"panicked"
}
))),
}
}
async fn finish_put(
ctx: Arc<OpsContext>,
namespace: &str,
put_task: tokio::task::JoinHandle<Result<(Key, Pin), Error>>,
publisher: Option<FanoutPublisher>,
) -> Result<Value, CallError> {
let (key, pin) = match put_task.await {
Ok(Ok(landing)) => landing,
Ok(Err(error)) => {
return Err(put_refused_error("the commit verification", None, error));
}
Err(join_error) => {
return Err(CallError::internal(format!(
"the put's store task failed ({})",
if join_error.is_cancelled() {
"cancelled"
} else {
"panicked"
}
)));
}
};
if let Some(publisher) = publisher {
publisher.finish();
}
let key_hex = hex::encode(key.as_bytes());
let token = match ctx.fleet_owner() {
Some(owner) => match ctx.mint_fleet(namespace, key, &owner) {
Ok(token) => {
drop(pin);
token
}
Err(Error::RegistryFull) => return Err(mint_refused_error(&key_hex)),
Err(_) => {
drop(pin);
return Err(CallError::internal("the fleet token mint refused"));
}
},
None => match ctx.mint_custodial(namespace, key, pin) {
Ok(token) => token,
Err(Error::RegistryFull) => return Err(mint_refused_error(&key_hex)),
Err(_) => return Err(CallError::internal("the custodial token mint refused")),
},
};
Ok(json!({
"token": token.to_hex(),
"key": key_hex,
}))
}