Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4842388
Armed a stream from a Nexus read that waits and finds nothing.
moetemp Oct 14, 2026
06ab5a7
Merged cn4-nexus-stream-calls into an3-read-arm.
moetemp Oct 14, 2026
6a038b0
Merged cn4-nexus-stream-calls into an3-read-arm.
moetemp Oct 14, 2026
4d7a435
Merged cn4-nexus-stream-calls into an3-read-arm.
moetemp Oct 14, 2026
36cfa2e
Merged cn4-nexus-stream-calls into an3-read-arm.
moetemp Oct 14, 2026
ebf35c8
Merged cn4-nexus-stream-calls into an3-read-arm.
moetemp Oct 14, 2026
65eeb53
Merged cn4-nexus-stream-calls into an3-read-arm.
moetemp Oct 14, 2026
9520d16
Merged cn4-nexus-stream-calls into an3-read-arm.
moetemp Oct 14, 2026
6ac364c
Merged cn4-nexus-stream-calls into an3-read-arm.
moetemp Oct 14, 2026
49f1fec
Merged cn4-nexus-stream-calls into an3-read-arm.
moetemp Oct 15, 2026
e83ccd8
Merged cn4-nexus-stream-calls into an3-read-arm.
moetemp Oct 15, 2026
1d9f7cb
Merged cn4-nexus-stream-calls into an3-read-arm.
moetemp Oct 15, 2026
2b10ae2
Merged cn4-nexus-stream-calls into an3-read-arm.
moetemp Oct 15, 2026
64b5cb8
Merged cn4-nexus-stream-calls into an3-read-arm.
moetemp Oct 15, 2026
f0fc726
Merged cn4-nexus-stream-calls into an3-read-arm.
moetemp Oct 15, 2026
8ede0e0
Merged cn4-nexus-stream-calls into an3-read-arm.
moetemp Oct 15, 2026
026d8f6
Merged cn4-nexus-stream-calls into an3-read-arm.
moetemp Oct 15, 2026
d4763e5
Merged cn4-nexus-stream-calls into an3-read-arm.
moetemp Oct 16, 2026
03fb443
Merged cn4-nexus-stream-calls into an3-read-arm.
moetemp Oct 16, 2026
c9d532b
Merged cn4-nexus-stream-calls into an3-read-arm.
moetemp Oct 16, 2026
5f82b8a
Merged cn4-nexus-stream-calls into an3-read-arm.
moetemp Oct 16, 2026
6bc366d
Merged cn4-nexus-stream-calls into an3-read-arm.
moetemp Oct 16, 2026
03ddc70
Merged cn4 into an3.
moetemp Oct 16, 2026
e55f6cc
Merged cn4 into an3.
moetemp Oct 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions crates/streams/nexus/temporal_streams.nexusrpc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,13 @@ $defs:
producer attempts the reader already saw, so its superseded and
stale marks are exact across calls and across handler Workers. A
state that doesn't parse is refused as a cursor mistake.
arm:
type: boolean
description: >-
For a reader that sleeps until its caller hears of progress. When
the call waits its whole wait_ms and collects nothing, the endpoint
arms the stream, so the next append notifies the stream's callers.
A reader with no callback leaves it out.
required: [stream]
additionalProperties: false

Expand Down Expand Up @@ -280,6 +287,12 @@ $defs:
also when the answer was cut short by its byte budget. It holds at
most 16 KiB, which is about 22 KiB as base64, so it can ride in a
Workflow's History. A longer state is refused.
armed:
type: boolean
description: >-
The call asked to arm, collected nothing and armed the stream, so
the caller waits for progress. Never true with records or with done.
An empty answer without it falls back to waiting for progress too.
required: [records, next_token, done]
additionalProperties: true

Expand Down
6 changes: 6 additions & 0 deletions crates/streams/src/nexus/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ impl NexusCalls {
next_token: latest.cursor,
done: false,
state: None,
armed: false,
}),
Err(error) => failed(failure::of_stream(&error)),
};
Expand Down Expand Up @@ -640,6 +641,7 @@ impl Budget {
next_token: read.response.cursor.clone(),
done: false,
state: Some(Base64::default()),
armed: false,
});
let state = crossing_size(read.state.encoded_len().min(MAX_STATE_BYTES));
Self {
Expand Down Expand Up @@ -714,6 +716,8 @@ fn finish_read(
wires.push(wire);
kept.extend(stored);
}
// Core's read no longer arms, so the contract's `armed` stays false until it leaves the IDL.
let armed = false;
let (next_token, done) = if cut {
// A supersession belongs to the record after it, so it goes into the next answer too.
if wires.last().is_some_and(|wire| wire.superseded.is_some()) {
Expand All @@ -734,6 +738,7 @@ fn finish_read(
next_token,
done,
state: Some(Base64(state.encode_to_vec())),
armed,
})
}

Expand Down Expand Up @@ -807,6 +812,7 @@ fn read_failure(error: StreamError, state: Vec<u8>) -> NexusStep {
next_token: cursor,
done: false,
state: (!state.is_empty()).then_some(Base64(state)),
armed: false,
}),
_ => failed(failure::of_stream(&error)),
}
Expand Down
4 changes: 4 additions & 0 deletions crates/streams/src/nexus/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ pub(crate) struct ReadInput {
/// Left as base64, so an oversized state is refused before it is decoded.
#[serde(default)]
pub(crate) state: Option<String>,
#[serde(default)]
pub(crate) arm: Option<bool>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
Expand All @@ -105,6 +107,8 @@ pub(crate) struct ReadOutput {
pub(crate) done: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) state: Option<Base64>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub(crate) armed: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
Expand Down
2 changes: 2 additions & 0 deletions crates/streams/tests/nexus_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ fn full_input(contract: &Value, name: &str, workflow_id: &str) -> Value {
("ReadInput", "wait_ms") => json!(0),
("ReadInput", "latest_only") => json!(false),
("ReadInput", "state") => Value::Null,
// So the answer carries `armed`, which the output check holds to the contract.
("ReadInput", "arm") => json!(true),
(model, member) => panic!("the contract added {model}.{member}; teach this test"),
};
out.insert(member, value);
Expand Down
15 changes: 15 additions & 0 deletions crates/streams/tests/nexus_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,20 @@ mod cases {
assert!(small.get("error").is_none());
assert_eq!(texts(&answer), ["small"]);
}
fn arming(wf: &str, after: &str) -> Value {
let mut input = read_input(wf, after);
input["arm"] = json!(true);
input
}

pub(crate) async fn a_read_that_asks_to_arm_is_never_armed(store: Arc<dyn StreamStore>) {
let calls = service(store, NexusOptions::default());
let wf = workflow();
let answer = read(&calls, arming(&wf, "")).await.unwrap();
assert!(records(&answer).is_empty());
assert!(answer.get("armed").is_none(), "{answer}");
assert_eq!(answer["done"], false);
}
}

macro_rules! lanes {
Expand Down Expand Up @@ -738,6 +752,7 @@ macro_rules! all_cases {
a_record_above_the_budget_still_crosses_alone,
an_append_above_the_record_limit_is_refused,
a_stored_record_above_the_limit_crosses_as_an_error,
a_read_that_asks_to_arm_is_never_armed,
);
};
}
Expand Down
Loading