create-feltdb 0.7.0 → 0.7.1

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.
@@ -1,4 +1,4 @@
1
1
  // One release train keeps generated applications installable. The repository
2
2
  // validation script checks these values against every workspace manifest.
3
- export const FELTDB_PACKAGE_VERSION = '0.7.0';
3
+ export const FELTDB_PACKAGE_VERSION = '0.7.1';
4
4
  export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;
@@ -1175,18 +1175,27 @@ impl FeltDb {
1175
1175
  }
1176
1176
  }
1177
1177
 
1178
- // Every precondition held. A write fenced by `expected_version` is a
1179
- // conditional replacement and behaves as `updateIfVersion` does:
1180
- // the authority validated version N above and writes N + 1 here,
1181
- // inside the same lock, so the check and the advance cannot be
1182
- // separated by another writer.
1178
+ // Every precondition held. The authority owns both ends of the
1179
+ // public document-version lifecycle inside this lock:
1183
1180
  //
1184
- // Only a version-fenced write advances. An unconditional write
1185
- // stores the caller's object verbatim, which is what preserves
1186
- // every existing transaction caller, and a guard-only precondition
1187
- // writes nothing at all. See
1181
+ // - a create protected by `requireAbsent` writes version 1;
1182
+ // - a write fenced by `expected_version = N` writes N + 1.
1183
+ //
1184
+ // An unconditional write remains verbatim, and a guard-only
1185
+ // precondition writes nothing. See
1188
1186
  // docs/architecture/transaction-version-contract.md.
1189
- let mut advanced: Vec<AtomicMutation> = Vec::new();
1187
+ let advanced: Vec<AtomicMutation>;
1188
+ let creates: HashSet<(&str, &str)> = preconditions
1189
+ .iter()
1190
+ .filter(|condition| condition.expected_version.is_none())
1191
+ .map(|condition| (condition.capability.as_str(), condition.key.as_str()))
1192
+ .chain(
1193
+ record_preconditions
1194
+ .iter()
1195
+ .filter(|condition| condition.require_absent)
1196
+ .map(|condition| (condition.capability.as_str(), condition.key.as_str())),
1197
+ )
1198
+ .collect();
1190
1199
  let fenced: HashMap<(&str, &str), u64> = record_preconditions
1191
1200
  .iter()
1192
1201
  .filter_map(|condition| {
@@ -1195,15 +1204,19 @@ impl FeltDb {
1195
1204
  .map(|version| ((condition.capability.as_str(), condition.key.as_str()), version))
1196
1205
  })
1197
1206
  .collect();
1198
- let mutations: &[AtomicMutation] = if fenced.is_empty() {
1207
+ let mutations: &[AtomicMutation] = if fenced.is_empty() && creates.is_empty() {
1199
1208
  mutations
1200
1209
  } else {
1201
1210
  advanced = mutations
1202
1211
  .iter()
1203
1212
  .map(|mutation| {
1204
- let Some(expected) =
1205
- fenced.get(&(mutation.capability.as_str(), mutation.key.as_str()))
1206
- else {
1213
+ let record = (mutation.capability.as_str(), mutation.key.as_str());
1214
+ let version = if creates.contains(&record) {
1215
+ Some(1)
1216
+ } else {
1217
+ fenced.get(&record).map(|expected| expected + 1)
1218
+ };
1219
+ let Some(version) = version else {
1207
1220
  return mutation.clone();
1208
1221
  };
1209
1222
  // A delete has no record left to carry a version, so a
@@ -1212,12 +1225,9 @@ impl FeltDb {
1212
1225
  return mutation.clone();
1213
1226
  };
1214
1227
  let mut fields = fields;
1215
- // The authority owns the transition. A `__version` the
1216
- // caller put in the value is replaced rather than
1217
- // honoured, exactly as updateIfVersion strips it,
1218
- // because a caller that computes a different next
1219
- // version must not be able to install it.
1220
- fields.insert("__version".to_string(), Value::from(expected + 1));
1228
+ // The caller cannot manufacture either the initial or
1229
+ // next authoritative version.
1230
+ fields.insert("__version".to_string(), Value::from(version));
1221
1231
  AtomicMutation {
1222
1232
  capability: mutation.capability.clone(),
1223
1233
  key: mutation.key.clone(),
@@ -28,12 +28,9 @@
28
28
  //! different numbers that happen to share a name, and fencing on the wrong one
29
29
  //! would compare a caller's value against something the caller never sees.
30
30
  //!
31
- //! `__version` is owned by the writer: the client sets it to 1 on insert and
32
- //! single-record CAS increments it. A transaction `set` writes the value it is
33
- //! given, so a caller that wants successive fenced transactions to exclude each
34
- //! other must advance `__version` in the value it writes. That is asserted
35
- //! below rather than assumed, because a fence that silently stops fencing is
36
- //! worse than no fence.
31
+ //! The authority owns conditional transaction versions: `requireAbsent`
32
+ //! creates version 1 and a version-fenced write of N stores N + 1. Both happen
33
+ //! inside the same lock as validation. Unconditional writes remain verbatim.
37
34
 
38
35
  #[cfg(test)]
39
36
  mod tests {
@@ -401,6 +398,7 @@ mod tests {
401
398
  guarded(&db, "tx-new", &[absent("fresh")], &[set("cells", "fresh", json!({ "id": "fresh" }))])
402
399
  .expect("absent record commits");
403
400
  assert!(present(&db, "cells", "fresh"));
401
+ assert_eq!(version_of(&db, "cells", "fresh"), 1);
404
402
 
405
403
  let error = guarded(
406
404
  &db,
@@ -807,9 +805,7 @@ mod tests {
807
805
  }
808
806
 
809
807
  #[test]
810
- fn a_transaction_insert_keeps_the_callers_value_and_the_authority_assigns_nothing() {
811
- // Insert remains the client's: Collection.insert supplies __version 1,
812
- // which is where record versioning begins.
808
+ fn a_transaction_create_gets_authority_version_one_and_can_be_fenced_immediately() {
813
809
  let dir = TempDir::new().unwrap();
814
810
  let db = db(&dir);
815
811
 
@@ -817,10 +813,19 @@ mod tests {
817
813
  &db,
818
814
  "tx-insert",
819
815
  &[RecordPrecondition { require_absent: true, ..precondition("cells", "new") }],
820
- &[set("cells", "new", json!({ "id": "new", "__version": 1 }))],
816
+ &[set("cells", "new", json!({ "id": "new", "__version": 999, "state": "created" }))],
821
817
  )
822
818
  .expect("commits");
823
- assert_eq!(version_of(&db, "cells", "new"), 1, "the caller's value, stored as given");
819
+ assert_eq!(version_of(&db, "cells", "new"), 1, "the authority owns the initial version");
820
+
821
+ guarded(
822
+ &db,
823
+ "tx-update-new",
824
+ &[expect_version("cells", "new", 1)],
825
+ &[set("cells", "new", json!({ "id": "new", "state": "updated" }))],
826
+ )
827
+ .expect("the newly created version can be fenced immediately");
828
+ assert_eq!(version_of(&db, "cells", "new"), 2);
824
829
 
825
830
  let missing = guarded(
826
831
  &db,
@@ -834,6 +839,33 @@ mod tests {
834
839
  );
835
840
  }
836
841
 
842
+ #[test]
843
+ fn replayed_conditional_create_does_not_create_or_advance_twice() {
844
+ let dir = TempDir::new().unwrap();
845
+ let db = db(&dir);
846
+ let absent = RecordPrecondition { require_absent: true, ..precondition("cells", "new") };
847
+
848
+ let first = guarded(
849
+ &db,
850
+ "tx-create-once",
851
+ &[absent.clone()],
852
+ &[set("cells", "new", json!({ "id": "new", "delivery": 1 }))],
853
+ )
854
+ .expect("first delivery commits");
855
+ assert!(!first.duplicate);
856
+
857
+ let replay = guarded(
858
+ &db,
859
+ "tx-create-once",
860
+ &[absent],
861
+ &[set("cells", "new", json!({ "id": "new", "delivery": 2 }))],
862
+ )
863
+ .expect("replay returns the original transaction result");
864
+ assert!(replay.duplicate);
865
+ assert_eq!(version_of(&db, "cells", "new"), 1);
866
+ assert_eq!(field(&db, "cells", "new", "delivery"), 1);
867
+ }
868
+
837
869
  #[test]
838
870
  fn replay_of_a_fenced_transaction_does_not_advance_twice() {
839
871
  let dir = TempDir::new().unwrap();
@@ -41,7 +41,7 @@ use feltdb::{
41
41
  Grant, GrantSigner, GrantStore, Subject as GrantSubject,
42
42
  },
43
43
  cardinality_endpoint::{CardinalityContext, CardinalityDiagnosticResponse},
44
- AtomicMutation, AtomicPrecondition,
44
+ AtomicMutation,
45
45
  policy_evaluation::{Actor, PolicyContext, PolicyEvaluator, PolicySubject},
46
46
  state_contract::{
47
47
  begin_read, compare_schemas, execute_query as execute_state_query, execute_transaction,
@@ -9134,7 +9134,6 @@ async fn commit_transaction(
9134
9134
  // Validate every operation before any of them is turned into a mutation, so
9135
9135
  // a malformed request cannot commit a prefix of the transaction.
9136
9136
  let mut mutations = Vec::with_capacity(request.operations.len());
9137
- let mut preconditions = Vec::new();
9138
9137
  let mut record_preconditions: Vec<RecordPrecondition> = Vec::new();
9139
9138
  let mut seen = std::collections::HashSet::new();
9140
9139
 
@@ -9173,14 +9172,17 @@ async fn commit_transaction(
9173
9172
  ));
9174
9173
  }
9175
9174
  if operation.require_absent {
9176
- // The store reads a precondition with no expected version as
9177
- // "this record must not exist", and evaluates it inside the same
9178
- // lock as the commit, so there is no window between the check and
9179
- // the write.
9180
- preconditions.push(AtomicPrecondition {
9175
+ // Use the public record-precondition path so a lost creation race
9176
+ // is both evaluated under the store lock and returned as a
9177
+ // structured presence conflict. The same condition tells the
9178
+ // authority to assign document version 1 to the created value.
9179
+ record_preconditions.push(RecordPrecondition {
9181
9180
  capability: operation.collection.clone(),
9182
9181
  key: key.clone(),
9182
+ require_absent: true,
9183
9183
  expected_version: None,
9184
+ expected_epoch: None,
9185
+ expected_lease_id: None,
9184
9186
  });
9185
9187
  }
9186
9188
  // A guard attached to a staged write. Previously declared by the
@@ -9216,7 +9218,7 @@ async fn commit_transaction(
9216
9218
  &request.transaction_id,
9217
9219
  None,
9218
9220
  None,
9219
- &preconditions,
9221
+ &[],
9220
9222
  &record_preconditions,
9221
9223
  &mutations,
9222
9224
  None,
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.7.0",
5
+ "version": "0.7.1",
6
6
  "license": "MIT",
7
7
  "bin": {
8
8
  "create-feltdb": "bin/create-feltdb.js"