feat: release Dever 0.1.5 with direct workers and long transactions
This commit is contained in:
1 parent
89629c2197
commit
a900d7e8db
153 files changed
+3676
-3708
No files matched your search
@@ -99,11 +99,13 @@ pub(super) fn validate_rest_bindings(program: &Program, errors: &mut Vec<Diagnos
|
||||
ExpressionKind::Call {
|
||||
target: CallTarget::Function(target),
|
||||
..
|
||||
} => {
|
||||
if !rest_function_allowed(program, *target, &model_domain) {
|
||||
errors.push(Diagnostic::error("C013", "REST field bindings call only pure same-domain Domain functions or pure standard library functions", value.span));
|
||||
}
|
||||
} if !rest_function_allowed(program, *target, &model_domain) => {
|
||||
errors.push(Diagnostic::error("C013", "REST field bindings call only pure same-domain Domain functions or pure standard library functions", value.span));
|
||||
}
|
||||
ExpressionKind::Call {
|
||||
target: CallTarget::Function(_),
|
||||
..
|
||||
} => {}
|
||||
ExpressionKind::Call { .. }
|
||||
| ExpressionKind::ModelOperation { .. }
|
||||
| ExpressionKind::JobEnqueue { .. }
|
||||
@@ -270,57 +272,44 @@ fn rest_intrinsic_allowed(operation: crate::intrinsic::Intrinsic) -> bool {
|
||||
}
|
||||
|
||||
pub(super) fn validate_write_transactions(program: &Program, errors: &mut Vec<Diagnostic>) {
|
||||
let write_routes = program
|
||||
let roots = program
|
||||
.api_routes
|
||||
.iter()
|
||||
.filter(|route| route.method != "GET")
|
||||
.map(|route| route.function)
|
||||
.map(|route| (route.function, route.long))
|
||||
.chain(
|
||||
program
|
||||
.api_commands
|
||||
.iter()
|
||||
.map(|command| (command.function, command.long)),
|
||||
)
|
||||
.chain(program.jobs.iter().map(|job| (job.function, job.long)))
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
for function in &write_routes {
|
||||
let root = crate::specialize::Specialization {
|
||||
function: *function,
|
||||
handlers: Vec::new(),
|
||||
};
|
||||
let password_crypto = crate::specialize::uses_intrinsic(
|
||||
program,
|
||||
&root,
|
||||
crate::intrinsic::Intrinsic::CryptoPasswordVerify,
|
||||
) || crate::specialize::uses_intrinsic(
|
||||
program,
|
||||
&root,
|
||||
crate::intrinsic::Intrinsic::CryptoPasswordHash,
|
||||
);
|
||||
if password_crypto
|
||||
&& let Some(span) = crate::specialize::write_outside_transaction(program, &root)
|
||||
{
|
||||
let mut error = Diagnostic::error(
|
||||
"C014",
|
||||
"password crypto API writes must be enclosed in an explicit transaction function",
|
||||
program.functions[*function].span,
|
||||
);
|
||||
error.related.push(crate::diagnostic::Label {
|
||||
span,
|
||||
message: "database write occurs outside the short transaction boundary".into(),
|
||||
});
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
let roots = write_routes
|
||||
.into_iter()
|
||||
.chain(program.api_commands.iter().map(|command| command.function))
|
||||
.chain(program.jobs.iter().map(|job| job.function))
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
for function in roots {
|
||||
for (function, long) in roots {
|
||||
let root = crate::specialize::Specialization {
|
||||
function,
|
||||
handlers: Vec::new(),
|
||||
};
|
||||
if crate::specialize::writes_database(program, &root)
|
||||
if long {
|
||||
if let Some(span) = crate::specialize::write_outside_transaction(program, &root) {
|
||||
let mut error = Diagnostic::error(
|
||||
"C014",
|
||||
"long entry database writes must be enclosed in an explicit transaction function",
|
||||
program.functions[function].span,
|
||||
);
|
||||
error.related.push(crate::diagnostic::Label {
|
||||
span,
|
||||
message: "database write occurs outside the explicit transaction boundary"
|
||||
.into(),
|
||||
});
|
||||
errors.push(error);
|
||||
}
|
||||
} else if crate::specialize::writes_database(program, &root)
|
||||
&& crate::specialize::starts_transaction_unsafe_work(program, &root)
|
||||
{
|
||||
errors.push(Diagnostic::error(
|
||||
"C014",
|
||||
"an API, CMD, or Job database transaction cannot start concurrent work",
|
||||
"automatic entry transactions cannot contain external I/O, Workers, waits, concurrency, or password crypto; declare the entry 'long' and enclose its writes in explicit transaction functions",
|
||||
program.functions[function].span,
|
||||
));
|
||||
}
|
||||
@@ -526,6 +515,7 @@ pub(super) fn register(
|
||||
let path = format!("/{prefix}/{}", binding.action.text);
|
||||
if occupy(&mut routes, (method, path.clone()), binding.span, "HTTP route", errors) {
|
||||
entries.routes.push(ApiRoute { method, path, function: id, inputs,
|
||||
long: binding.long,
|
||||
directory: directory.clone(), anonymous: binding.anonymous,
|
||||
component: component.clone(), domain: domain.clone(),
|
||||
action: binding.action.text.clone(), auth: None, permission: None,
|
||||
@@ -536,7 +526,7 @@ pub(super) fn register(
|
||||
} else {
|
||||
let name = format!("{domain_name}.{}", binding.action.text);
|
||||
if occupy(&mut commands, name.clone(), binding.span, "command", errors) {
|
||||
entries.commands.push(ApiCommand { name, function: id, inputs, components: Vec::new() });
|
||||
entries.commands.push(ApiCommand { name, function: id, long: binding.long, inputs, components: Vec::new() });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -981,6 +971,7 @@ fn legacy_routes(
|
||||
inputs,
|
||||
directory: Vec::new(),
|
||||
anonymous: false,
|
||||
long: false,
|
||||
span: clause.span,
|
||||
component: function.name.split('.').next().unwrap_or("app").to_owned(),
|
||||
domain: function.name.split('.').nth(1).unwrap_or("main").to_owned(),
|
||||
|
||||
@@ -113,18 +113,19 @@ pub(super) fn register(
|
||||
let mut scheduled = BTreeSet::new();
|
||||
for declaration in &package.declarations {
|
||||
match declaration {
|
||||
Declaration::Database(binding) if is_job => {
|
||||
if connection
|
||||
.replace(ConnectionSelector::Explicit(binding.name.text.clone()))
|
||||
.is_some()
|
||||
{
|
||||
errors.push(Diagnostic::error(
|
||||
"C002",
|
||||
"duplicate Job database binding",
|
||||
binding.span,
|
||||
));
|
||||
}
|
||||
Declaration::Database(binding)
|
||||
if is_job
|
||||
&& connection
|
||||
.replace(ConnectionSelector::Explicit(binding.name.text.clone()))
|
||||
.is_some() =>
|
||||
{
|
||||
errors.push(Diagnostic::error(
|
||||
"C002",
|
||||
"duplicate Job database binding",
|
||||
binding.span,
|
||||
));
|
||||
}
|
||||
Declaration::Database(_) if is_job => {}
|
||||
Declaration::Function(clause) => {
|
||||
let declared = matches!(clause.kind, FunctionKind::Job { .. });
|
||||
if is_job != declared {
|
||||
@@ -159,6 +160,7 @@ pub(super) fn register(
|
||||
let FunctionKind::Job {
|
||||
attempts,
|
||||
timeout_ms,
|
||||
long,
|
||||
} = entry.kind
|
||||
else {
|
||||
continue;
|
||||
@@ -210,6 +212,7 @@ pub(super) fn register(
|
||||
};
|
||||
jobs.push(Job {
|
||||
function,
|
||||
long,
|
||||
connection: connection.clone(),
|
||||
payload,
|
||||
attempts,
|
||||
|
||||
@@ -525,23 +525,19 @@ fn validate_model_declarations(
|
||||
let mut sql = BTreeSet::new();
|
||||
for declaration in &package.declarations {
|
||||
match declaration {
|
||||
Declaration::Database(binding) => {
|
||||
if database.replace(binding.span).is_some() {
|
||||
errors.push(Diagnostic::error(
|
||||
"C002",
|
||||
"a Model may declare only one database binding",
|
||||
binding.span,
|
||||
));
|
||||
}
|
||||
Declaration::Database(binding) if database.replace(binding.span).is_some() => {
|
||||
errors.push(Diagnostic::error(
|
||||
"C002",
|
||||
"a Model may declare only one database binding",
|
||||
binding.span,
|
||||
));
|
||||
}
|
||||
Declaration::Seed(value) => {
|
||||
if seed.replace(value.span).is_some() {
|
||||
errors.push(Diagnostic::error(
|
||||
"C002",
|
||||
"a Model may declare only one seed block",
|
||||
value.span,
|
||||
));
|
||||
}
|
||||
Declaration::Seed(value) if seed.replace(value.span).is_some() => {
|
||||
errors.push(Diagnostic::error(
|
||||
"C002",
|
||||
"a Model may declare only one seed block",
|
||||
value.span,
|
||||
));
|
||||
}
|
||||
Declaration::Migration(migration) => {
|
||||
if !migrations.insert(&migration.name.text) {
|
||||
|
||||
@@ -548,12 +548,7 @@ pub(super) fn referenced_types(ty: &Type, visit: &mut impl FnMut(usize)) {
|
||||
| Type::Stream(element)
|
||||
| Type::AsyncStream(element)
|
||||
| Type::RowStream(element)
|
||||
| Type::Related(element)
|
||||
| Type::Channel(element) => {
|
||||
if !matches!(ty, Type::Related(_)) {
|
||||
referenced_types(element, visit)
|
||||
}
|
||||
}
|
||||
| Type::Channel(element) => referenced_types(element, visit),
|
||||
Type::Map(key, value) | Type::MapEntry(key, value) => {
|
||||
referenced_types(key, visit);
|
||||
referenced_types(value, visit);
|
||||
|
||||
@@ -588,7 +588,7 @@ fn validate_transactions(program: &Program, summaries: &[Effects], errors: &mut
|
||||
if concurrent {
|
||||
errors.push(Diagnostic::error(
|
||||
"C014",
|
||||
"transaction functions cannot start concurrent work",
|
||||
"transaction functions cannot start concurrent work or external I/O",
|
||||
function.span,
|
||||
));
|
||||
}
|
||||
@@ -669,7 +669,7 @@ fn validate_transactions(program: &Program, summaries: &[Effects], errors: &mut
|
||||
for function in concurrent {
|
||||
errors.push(Diagnostic::error(
|
||||
"C014",
|
||||
"transaction functions cannot start concurrent work",
|
||||
"transaction functions cannot start concurrent work or external I/O",
|
||||
program.functions[function].span,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -117,6 +117,9 @@ impl Formatter {
|
||||
if binding.anonymous {
|
||||
parts.extend([self.token("public"), Doc::space()]);
|
||||
}
|
||||
if binding.long {
|
||||
parts.extend([self.token("long"), Doc::space()]);
|
||||
}
|
||||
parts.extend(vec![
|
||||
self.token(binding.kind.keyword()),
|
||||
Doc::space(),
|
||||
@@ -377,6 +380,9 @@ impl Formatter {
|
||||
parts.extend([self.token("transaction"), Doc::space()]);
|
||||
}
|
||||
if matches!(function.kind, FunctionKind::Job { .. }) {
|
||||
if matches!(function.kind, FunctionKind::Job { long: true, .. }) {
|
||||
parts.extend([self.token("long"), Doc::space()]);
|
||||
}
|
||||
parts.extend([self.token("job"), Doc::space()]);
|
||||
}
|
||||
for (index, segment) in function.name.text.split('.').enumerate() {
|
||||
@@ -394,6 +400,7 @@ impl Formatter {
|
||||
if let FunctionKind::Job {
|
||||
attempts,
|
||||
timeout_ms,
|
||||
..
|
||||
} = function.kind
|
||||
{
|
||||
for (name, value) in [("retry", attempts), ("timeout", timeout_ms)] {
|
||||
|
||||
@@ -44,6 +44,7 @@ pub(crate) struct AuthHook {
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Job {
|
||||
pub function: usize,
|
||||
pub long: bool,
|
||||
pub connection: crate::model::ConnectionSelector,
|
||||
pub payload: Option<crate::wire::Schema>,
|
||||
pub attempts: u32,
|
||||
@@ -148,6 +149,7 @@ impl TestCase {
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ApiRoute {
|
||||
pub multipart: bool,
|
||||
pub long: bool,
|
||||
pub method: &'static str,
|
||||
pub path: String,
|
||||
pub function: usize,
|
||||
@@ -166,6 +168,7 @@ pub(crate) struct ApiRoute {
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ApiCommand {
|
||||
pub name: String,
|
||||
pub long: bool,
|
||||
pub function: usize,
|
||||
pub inputs: Vec<String>,
|
||||
pub components: Vec<String>,
|
||||
|
||||
@@ -292,13 +292,7 @@ impl Module<'_> {
|
||||
};
|
||||
let mut emitter = FunctionEmitter::new(self, instance);
|
||||
let parameters = emitter.begin_generated_function(&wrapper);
|
||||
let password = [
|
||||
Intrinsic::CryptoPasswordHash,
|
||||
Intrinsic::CryptoPasswordVerify,
|
||||
]
|
||||
.iter()
|
||||
.any(|operation| specialize::uses_intrinsic(emitter.module.program, &target, *operation));
|
||||
let transaction = !password && emitter.begin_command_transaction(&target, route.span);
|
||||
let transaction = !route.long && emitter.begin_command_transaction(&target, route.span);
|
||||
let arguments = app
|
||||
.parameters
|
||||
.iter()
|
||||
|
||||
@@ -752,7 +752,7 @@ impl FunctionEmitter<'_, '_> {
|
||||
args.push((ty.clone(), value));
|
||||
}
|
||||
let output_ty = output_type(&function.outputs);
|
||||
let transactional = self.begin_command_transaction(&instance, span);
|
||||
let transactional = !command.long && self.begin_command_transaction(&instance, span);
|
||||
let (status, row) = self.invoke_values(&instance, &args, &output_ty, span);
|
||||
self.propagate_status(&status, span, false);
|
||||
let value = self.temp();
|
||||
|
||||
@@ -213,6 +213,7 @@ impl FunctionEmitter<'_, '_> {
|
||||
.program
|
||||
.api_commands
|
||||
.iter()
|
||||
.filter(|command| !command.long)
|
||||
.map(|command| Specialization {
|
||||
function: command.function,
|
||||
handlers: Vec::new(),
|
||||
@@ -224,7 +225,7 @@ impl FunctionEmitter<'_, '_> {
|
||||
.program
|
||||
.api_routes
|
||||
.iter()
|
||||
.filter(|route| matches!(route.method, "POST" | "PUT" | "DELETE"))
|
||||
.filter(|route| !route.long && matches!(route.method, "POST" | "PUT" | "DELETE"))
|
||||
.map(|route| Specialization {
|
||||
function: route.function,
|
||||
handlers: Vec::new(),
|
||||
@@ -248,6 +249,7 @@ impl FunctionEmitter<'_, '_> {
|
||||
.program
|
||||
.jobs
|
||||
.iter()
|
||||
.filter(|job| !job.long)
|
||||
.map(|job| Specialization {
|
||||
function: job.function,
|
||||
handlers: Vec::new(),
|
||||
|
||||
@@ -188,7 +188,8 @@ impl Module<'_> {
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let transaction = emitter.begin_command_transaction(&target, function.span);
|
||||
let transaction = !emitter.module.program.jobs[index].long
|
||||
&& emitter.begin_command_transaction(&target, function.span);
|
||||
if specialize::suspends(emitter.module.program, &target) {
|
||||
let (status, _) =
|
||||
emitter.invoke_values(&target, &arguments, &Type::Unit, function.span);
|
||||
|
||||
@@ -699,9 +699,22 @@ fn emit_database_setup(
|
||||
for function in program
|
||||
.api_routes
|
||||
.iter()
|
||||
.filter(|route| !route.long)
|
||||
.map(|route| route.function)
|
||||
.chain(program.api_commands.iter().map(|command| command.function))
|
||||
.chain(program.jobs.iter().map(|job| job.function))
|
||||
.chain(
|
||||
program
|
||||
.api_commands
|
||||
.iter()
|
||||
.filter(|command| !command.long)
|
||||
.map(|command| command.function),
|
||||
)
|
||||
.chain(
|
||||
program
|
||||
.jobs
|
||||
.iter()
|
||||
.filter(|job| !job.long)
|
||||
.map(|job| job.function),
|
||||
)
|
||||
{
|
||||
if function_writes_database(program, function) {
|
||||
transaction_roots.insert(Specialization {
|
||||
|
||||
@@ -352,16 +352,7 @@ fn emit_handler(
|
||||
function: route.function,
|
||||
handlers: Vec::new(),
|
||||
};
|
||||
let password_crypto = specialize::uses_intrinsic(
|
||||
program,
|
||||
&target,
|
||||
crate::intrinsic::Intrinsic::CryptoPasswordVerify,
|
||||
) || specialize::uses_intrinsic(
|
||||
program,
|
||||
&target,
|
||||
crate::intrinsic::Intrinsic::CryptoPasswordHash,
|
||||
);
|
||||
let transaction_database = (!password_crypto)
|
||||
let transaction_database = (!route.long)
|
||||
.then(|| super::transaction_database(program, route.function))
|
||||
.flatten();
|
||||
if let Some(database) = &transaction_database {
|
||||
@@ -478,7 +469,9 @@ pub(super) fn emit_commands(program: &Program, output: &mut String) {
|
||||
function: command.function,
|
||||
handlers: Vec::new(),
|
||||
};
|
||||
let transaction_database = super::transaction_database(program, command.function);
|
||||
let transaction_database = (!command.long)
|
||||
.then(|| super::transaction_database(program, command.function))
|
||||
.flatten();
|
||||
if let Some(database) = &transaction_database {
|
||||
writeln!(output, "let transaction_database = ({database}).map_err(|error| AppError::database(\"command\", error))?;\nlet transaction = transaction_database.begin().await.map_err(|error| AppError::database(\"command\", error))?;")
|
||||
.expect("string formatting");
|
||||
|
||||
@@ -121,7 +121,9 @@ pub(super) fn definitions(
|
||||
function: *target,
|
||||
handlers: Vec::new(),
|
||||
};
|
||||
let transaction_database = super::transaction_database(emitter.program, *target);
|
||||
let transaction_database = (!job.long)
|
||||
.then(|| super::transaction_database(emitter.program, *target))
|
||||
.flatten();
|
||||
if let Some(database) = &transaction_database {
|
||||
writeln!(output, "let transaction_database = match {database} {{ Ok(database) => database, Err(_) => return Outcome::Retry }};\nlet transaction = match transaction_database.begin().await {{ Ok(value) => value, Err(_) => return Outcome::Retry }};")
|
||||
.expect("string formatting");
|
||||
|
||||
@@ -195,6 +195,30 @@ impl Parser {
|
||||
let mut errors = Vec::new();
|
||||
while !self.at(&Kind::End) {
|
||||
let visibility = self.take(&Kind::Public).map(|token| token.span);
|
||||
let long = if self.at_name("long")
|
||||
&& self
|
||||
.tokens
|
||||
.get(self.cursor + 1)
|
||||
.is_some_and(|token| token.kind != Kind::LeftParen)
|
||||
{
|
||||
Some(self.advance().span)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let long_entry = match layout.role() {
|
||||
Some(crate::source::SourceRole::Api) => ["get", "post", "put", "delete", "cmd"]
|
||||
.iter()
|
||||
.any(|name| self.at_name(name)),
|
||||
Some(crate::source::SourceRole::Job) => self.at_name("job"),
|
||||
_ => false,
|
||||
};
|
||||
if let Some(span) = long.filter(|_| !long_entry) {
|
||||
errors.push(Diagnostic::error(
|
||||
"P001",
|
||||
"'long' is allowed only before an HTTP, CMD, or Job entry; place 'public' before 'long'",
|
||||
span,
|
||||
));
|
||||
}
|
||||
let anonymous_http = layout.role() == Some(crate::source::SourceRole::Api)
|
||||
&& ["get", "post", "put", "delete"]
|
||||
.iter()
|
||||
@@ -264,8 +288,9 @@ impl Parser {
|
||||
FunctionKind::Job {
|
||||
attempts: 0,
|
||||
timeout_ms: 0,
|
||||
long: long.is_some(),
|
||||
},
|
||||
Some(start),
|
||||
Some(long.unwrap_or(start)),
|
||||
)
|
||||
.map(Declaration::Function)
|
||||
} else if self.at_name("transaction") {
|
||||
@@ -280,7 +305,8 @@ impl Parser {
|
||||
Ok(mut declaration) => {
|
||||
if let Declaration::Api(binding) = &mut declaration {
|
||||
binding.anonymous = visibility.is_some() && anonymous_http;
|
||||
if let Some(start) = visibility {
|
||||
binding.long = long.is_some();
|
||||
if let Some(start) = visibility.or(long) {
|
||||
binding.span = start.through(binding.span);
|
||||
}
|
||||
}
|
||||
@@ -409,6 +435,7 @@ impl Parser {
|
||||
self.line_end(&Kind::End)?;
|
||||
Ok(Declaration::Api(ApiBinding {
|
||||
anonymous: false,
|
||||
long: false,
|
||||
kind,
|
||||
action,
|
||||
target,
|
||||
@@ -925,13 +952,14 @@ impl Parser {
|
||||
let inputs = self.separated(Kind::RightParen, Self::input)?;
|
||||
self.expect(&Kind::LeftParen, "the named output list '('")?;
|
||||
let outputs = self.separated(Kind::RightParen, Self::field)?;
|
||||
if matches!(kind, FunctionKind::Job { .. }) {
|
||||
if let FunctionKind::Job { long, .. } = kind {
|
||||
let attempts = self.job_policy("retry", 100)?;
|
||||
let timeout_ms =
|
||||
self.job_policy("timeout", dever_runtime::config::MAX_JOB_TIMEOUT_MS)?;
|
||||
kind = FunctionKind::Job {
|
||||
attempts,
|
||||
timeout_ms,
|
||||
long,
|
||||
};
|
||||
}
|
||||
let (pure, recovery) = self.function_contracts()?;
|
||||
|
||||
@@ -172,9 +172,6 @@ pub(crate) fn uses_intrinsic(
|
||||
})
|
||||
}
|
||||
|
||||
/// Finds a database write reachable without first crossing an explicit
|
||||
/// transaction function. Password crypto APIs use this to keep slow work out
|
||||
/// of SQLite transactions while preserving a short, explicit write boundary.
|
||||
pub(crate) fn write_outside_transaction(
|
||||
program: &crate::hir::Program,
|
||||
root: &Specialization,
|
||||
@@ -237,7 +234,7 @@ pub(crate) fn function_starts_transaction_unsafe_work(
|
||||
program: &crate::hir::Program,
|
||||
function: usize,
|
||||
) -> bool {
|
||||
let mut unsafe_work = false;
|
||||
let mut unsafe_work = program.functions[function].external.is_some();
|
||||
crate::check::visit(&program.functions[function], |expression| {
|
||||
unsafe_work |= match &expression.kind {
|
||||
ExpressionKind::RunCall { .. }
|
||||
@@ -259,7 +256,17 @@ pub(crate) fn function_starts_transaction_unsafe_work(
|
||||
}
|
||||
ExpressionKind::Intrinsic { operation, .. } => {
|
||||
operation.is_async()
|
||||
&& !matches!(operation, crate::intrinsic::Intrinsic::UploadStore)
|
||||
|| matches!(
|
||||
operation,
|
||||
crate::intrinsic::Intrinsic::CryptoPasswordHash
|
||||
| crate::intrinsic::Intrinsic::CryptoPasswordVerify
|
||||
| crate::intrinsic::Intrinsic::FileOpen
|
||||
| crate::intrinsic::Intrinsic::TimeSleep
|
||||
| crate::intrinsic::Intrinsic::FileCreate
|
||||
| crate::intrinsic::Intrinsic::FileRead
|
||||
| crate::intrinsic::Intrinsic::FileWrite
|
||||
| crate::intrinsic::Intrinsic::FileChunks
|
||||
)
|
||||
}
|
||||
ExpressionKind::Collection {
|
||||
operation,
|
||||
|
||||
@@ -106,6 +106,7 @@ impl ApiKind {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ApiBinding {
|
||||
pub anonymous: bool,
|
||||
pub long: bool,
|
||||
pub kind: ApiKind,
|
||||
pub action: Name,
|
||||
pub target: Path,
|
||||
@@ -277,7 +278,11 @@ pub struct FunctionClause {
|
||||
pub enum FunctionKind {
|
||||
Ordinary,
|
||||
Transaction,
|
||||
Job { attempts: u32, timeout_ms: u32 },
|
||||
Job {
|
||||
attempts: u32,
|
||||
timeout_ms: u32,
|
||||
long: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
||||
Reference in new issue
Block a user