create-feltdb 0.6.13 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -57,7 +57,7 @@ use feltdb::{
57
57
  WorkerRegistration,
58
58
  },
59
59
  workload::{CreateWorkload, WorkloadStore},
60
- DatabaseSnapshot, FeltDb, JsonCasResult, Operation, PeerAdvertisement, PeerId, StoredRow,
60
+ DatabaseSnapshot, FeltDb, JsonCasResult, Operation, PeerAdvertisement, PeerId, StoredRow, FlowError, RecordPrecondition,
61
61
  };
62
62
  use feltdb_server::{
63
63
  app_state::AppState,
@@ -103,6 +103,16 @@ const PROTOCOL_VERSION: &str = "1";
103
103
  #[derive(Debug)]
104
104
  struct ApiError(StatusCode, String);
105
105
 
106
+ impl ApiError {
107
+ /// An error whose body is a structured document rather than a message.
108
+ ///
109
+ /// `api_error_body` already parses a message that happens to be JSON, so
110
+ /// this is the existing mechanism named rather than a second one.
111
+ fn structured(status: StatusCode, body: Value) -> Self {
112
+ Self(status, serde_json::to_string(&body).unwrap_or_else(|_| body.to_string()))
113
+ }
114
+ }
115
+
106
116
  impl IntoResponse for ApiError {
107
117
  fn into_response(self) -> Response {
108
118
  let body = api_error_body(self.0, self.1);
@@ -9039,6 +9049,38 @@ struct AtomicTxOperationRequest {
9039
9049
  /// When set, the record must not already exist for the transaction to commit.
9040
9050
  #[serde(default)]
9041
9051
  require_absent: bool,
9052
+ /// Fence this write on the record's current `__version`.
9053
+ ///
9054
+ /// Present because the client declares it; it was previously accepted by
9055
+ /// serde and silently discarded, so a caller could attach a stale version
9056
+ /// to a staged write and watch the transaction commit anyway.
9057
+ #[serde(default)]
9058
+ expected_version: Option<u64>,
9059
+ #[serde(default)]
9060
+ expected_epoch: Option<u64>,
9061
+ #[serde(default)]
9062
+ expected_lease_id: Option<String>,
9063
+ }
9064
+
9065
+ /// A precondition on a record the transaction does not necessarily write.
9066
+ ///
9067
+ /// Explicit rather than overloaded onto an operation, because read/decide/write
9068
+ /// fencing routinely guards on a record it does not modify -- a session
9069
+ /// generation, an ownership epoch -- and expressing that as a no-op write would
9070
+ /// make the guard a mutation.
9071
+ #[derive(Deserialize)]
9072
+ #[serde(rename_all = "camelCase")]
9073
+ struct AtomicTxPreconditionRequest {
9074
+ collection: String,
9075
+ id: String,
9076
+ #[serde(default)]
9077
+ require_absent: bool,
9078
+ #[serde(default)]
9079
+ expected_version: Option<u64>,
9080
+ #[serde(default)]
9081
+ expected_epoch: Option<u64>,
9082
+ #[serde(default)]
9083
+ expected_lease_id: Option<String>,
9042
9084
  }
9043
9085
 
9044
9086
  #[derive(Deserialize)]
@@ -9047,6 +9089,11 @@ struct AtomicTxRequest {
9047
9089
  /// Caller-supplied identity. Reusing one makes the commit idempotent.
9048
9090
  transaction_id: String,
9049
9091
  operations: Vec<AtomicTxOperationRequest>,
9092
+ /// Conditions the authority evaluates inside the same atomic boundary as
9093
+ /// the writes. Absent means an unconditional transaction, which behaves
9094
+ /// exactly as before.
9095
+ #[serde(default)]
9096
+ preconditions: Vec<AtomicTxPreconditionRequest>,
9050
9097
  }
9051
9098
 
9052
9099
  #[derive(Serialize)]
@@ -9088,7 +9135,35 @@ async fn commit_transaction(
9088
9135
  // a malformed request cannot commit a prefix of the transaction.
9089
9136
  let mut mutations = Vec::with_capacity(request.operations.len());
9090
9137
  let mut preconditions = Vec::new();
9138
+ let mut record_preconditions: Vec<RecordPrecondition> = Vec::new();
9091
9139
  let mut seen = std::collections::HashSet::new();
9140
+
9141
+ // Transaction-level preconditions first, on records the transaction need
9142
+ // not write at all.
9143
+ for condition in &request.preconditions {
9144
+ let key = record_key(&condition.collection, &condition.id)?;
9145
+ let precondition = RecordPrecondition {
9146
+ capability: condition.collection.clone(),
9147
+ key,
9148
+ require_absent: condition.require_absent,
9149
+ expected_version: condition.expected_version,
9150
+ expected_epoch: condition.expected_epoch,
9151
+ expected_lease_id: condition.expected_lease_id.clone(),
9152
+ };
9153
+ if precondition.is_empty() {
9154
+ // Refused rather than ignored. A precondition that constrains
9155
+ // nothing reads as protection and provides none, and silently
9156
+ // accepting one is how this whole class of defect happens.
9157
+ return Err(ApiError(
9158
+ StatusCode::BAD_REQUEST,
9159
+ format!(
9160
+ "precondition on {}/{} constrains nothing",
9161
+ condition.collection, condition.id
9162
+ ),
9163
+ ));
9164
+ }
9165
+ record_preconditions.push(precondition);
9166
+ }
9092
9167
  for operation in request.operations {
9093
9168
  let key = record_key(&operation.collection, &operation.id)?;
9094
9169
  if !seen.insert(key.clone()) {
@@ -9108,6 +9183,21 @@ async fn commit_transaction(
9108
9183
  expected_version: None,
9109
9184
  });
9110
9185
  }
9186
+ // A guard attached to a staged write. Previously declared by the
9187
+ // client, accepted by serde, and dropped here.
9188
+ if operation.expected_version.is_some()
9189
+ || operation.expected_epoch.is_some()
9190
+ || operation.expected_lease_id.is_some()
9191
+ {
9192
+ record_preconditions.push(RecordPrecondition {
9193
+ capability: operation.collection.clone(),
9194
+ key: key.clone(),
9195
+ require_absent: false,
9196
+ expected_version: operation.expected_version,
9197
+ expected_epoch: operation.expected_epoch,
9198
+ expected_lease_id: operation.expected_lease_id.clone(),
9199
+ });
9200
+ }
9111
9201
  let value = operation.value.map(|mut fields| {
9112
9202
  fields.insert("id".to_string(), Value::String(operation.id.clone()));
9113
9203
  Value::Object(fields)
@@ -9122,11 +9212,33 @@ async fn commit_transaction(
9122
9212
  let operations = mutations.len();
9123
9213
  let commit = state
9124
9214
  .db
9125
- .apply_atomic_transaction(&request.transaction_id, None, &preconditions, &mutations, None)
9215
+ .apply_atomic_transaction_guarded(
9216
+ &request.transaction_id,
9217
+ None,
9218
+ None,
9219
+ &preconditions,
9220
+ &record_preconditions,
9221
+ &mutations,
9222
+ None,
9223
+ )
9126
9224
  .map_err(|error| {
9127
9225
  // A refused transaction applied nothing, so this is a conflict, not
9128
- // a partially completed request.
9129
- ApiError(StatusCode::CONFLICT, error.to_string())
9226
+ // a partially completed request. A precondition failure carries
9227
+ // which predicate failed and what the authority holds, so a caller
9228
+ // can tell a lost race from a broken deployment without parsing
9229
+ // prose.
9230
+ match error {
9231
+ FlowError::PreconditionFailed(failure) => ApiError::structured(
9232
+ StatusCode::CONFLICT,
9233
+ json!({
9234
+ "committed": false,
9235
+ "code": "PRECONDITION_FAILED",
9236
+ "error": failure.to_string(),
9237
+ "failure": *failure,
9238
+ }),
9239
+ ),
9240
+ other => ApiError(StatusCode::CONFLICT, other.to_string()),
9241
+ }
9130
9242
  })?;
9131
9243
 
9132
9244
  Ok((
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "create-feltdb",
3
3
  "private": false,
4
4
  "description": "Create a new FeltDB application with one command",
5
- "version": "0.6.13",
5
+ "version": "0.7.0",
6
6
  "license": "MIT",
7
7
  "bin": {
8
8
  "create-feltdb": "bin/create-feltdb.js"