@feltdb/core 0.12.1 → 0.13.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.
Files changed (139) hide show
  1. package/dist/build-identity.json +13 -4
  2. package/dist/cli/commands.js +96 -45
  3. package/dist/cli/workspace-integration.js +8 -0
  4. package/dist/collection.d.ts +41 -4
  5. package/dist/collection.d.ts.map +1 -1
  6. package/dist/collection.js +116 -47
  7. package/dist/composition.d.ts.map +1 -1
  8. package/dist/composition.js +11 -1
  9. package/dist/create/build-identity.json +13 -4
  10. package/dist/create/cli.js +49 -40
  11. package/dist/create/create.js +32 -19
  12. package/dist/create/package-versions.js +1 -1
  13. package/dist/create/server-source/Cargo.lock +18 -3
  14. package/dist/create/server-source/Cargo.toml +4 -2
  15. package/dist/create/server-source/crates/feltdb/Cargo.toml +20 -0
  16. package/dist/create/server-source/crates/feltdb/LICENSE +21 -0
  17. package/dist/create/server-source/crates/feltdb/README.md +75 -0
  18. package/dist/create/server-source/crates/feltdb/src/agent_state.rs +320 -0
  19. package/dist/create/server-source/crates/feltdb/src/agent_transition_contract.rs +1545 -0
  20. package/dist/create/server-source/crates/feltdb/src/authority.rs +145 -15
  21. package/dist/create/server-source/crates/feltdb/src/lib.rs +227 -23
  22. package/dist/create/server-source/crates/feltdb/src/state_model.rs +1 -5
  23. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +204 -1
  24. package/dist/create/server-source/crates/feltdb/src/workload.rs +685 -53
  25. package/dist/create/server-source/crates/feltdb/tests/agent_state_native.rs +1188 -0
  26. package/dist/create/server-source/crates/feltdb/tests/agent_transition_contract_conformance.rs +2884 -0
  27. package/dist/create/server-source/crates/feltdb/tests/authority_transaction_identity.rs +534 -0
  28. package/dist/create/server-source/crates/feltdb/tests/fixtures/agent_transition_corpus.json +23340 -0
  29. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +18 -12
  30. package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +17 -5
  31. package/dist/create/server-source/crates/feltdb-client/Cargo.toml +29 -0
  32. package/dist/create/server-source/crates/feltdb-client/LICENSE +21 -0
  33. package/dist/create/server-source/crates/feltdb-client/README.md +70 -0
  34. package/dist/create/server-source/crates/feltdb-client/src/application.rs +527 -0
  35. package/dist/create/server-source/crates/feltdb-client/src/authority.rs +679 -0
  36. package/dist/create/server-source/crates/feltdb-client/src/control.rs +292 -0
  37. package/dist/create/server-source/crates/feltdb-client/src/error.rs +227 -0
  38. package/dist/create/server-source/crates/feltdb-client/src/events.rs +146 -0
  39. package/dist/create/server-source/crates/feltdb-client/src/lib.rs +466 -0
  40. package/dist/create/server-source/crates/feltdb-client/tests/fixtures/README.md +4 -0
  41. package/dist/create/server-source/crates/feltdb-client/tests/fixtures/private-ca.pem +20 -0
  42. package/dist/create/server-source/crates/feltdb-client/tests/fixtures/server-key.pem +28 -0
  43. package/dist/create/server-source/crates/feltdb-client/tests/fixtures/server.pem +20 -0
  44. package/dist/create/server-source/crates/feltdb-client/tests/private_ca.rs +95 -0
  45. package/dist/create/server-source/crates/feltdb-server/Cargo.toml +20 -1
  46. package/dist/create/server-source/crates/feltdb-server/LICENSE +21 -0
  47. package/dist/create/server-source/crates/feltdb-server/README.md +12 -1
  48. package/dist/create/server-source/crates/feltdb-server/src/agent_execution.rs +511 -0
  49. package/dist/create/server-source/crates/feltdb-server/src/auth.rs +28 -0
  50. package/dist/create/server-source/crates/feltdb-server/src/backup.rs +6 -2
  51. package/dist/create/server-source/crates/feltdb-server/src/lib.rs +21 -0
  52. package/dist/create/server-source/crates/feltdb-server/src/main.rs +845 -138
  53. package/dist/create/server-source/crates/feltdb-server/tests/rust_client.rs +683 -0
  54. package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +3 -1
  55. package/dist/create/setup-summary.js +40 -0
  56. package/dist/create/template/default-project/README.md +12 -7
  57. package/dist/create/template/default-project/src/pages/Dashboard.tsx +1 -1
  58. package/dist/create/template/dot-feltdb-README.md +3 -3
  59. package/dist/db.d.ts +27 -1
  60. package/dist/db.d.ts.map +1 -1
  61. package/dist/db.js +60 -21
  62. package/dist/development-runtime-bridge.d.ts +20 -0
  63. package/dist/development-runtime-bridge.d.ts.map +1 -1
  64. package/dist/development-runtime-bridge.js +74 -4
  65. package/dist/embedded-transaction.d.ts +75 -8
  66. package/dist/embedded-transaction.d.ts.map +1 -1
  67. package/dist/embedded-transaction.js +124 -18
  68. package/dist/error-codes.d.ts +2 -0
  69. package/dist/error-codes.d.ts.map +1 -1
  70. package/dist/error-codes.js +2 -0
  71. package/dist/feltdb.d.ts +1 -11
  72. package/dist/feltdb.d.ts.map +1 -1
  73. package/dist/file-db.d.ts +6 -0
  74. package/dist/file-db.d.ts.map +1 -1
  75. package/dist/file-db.js +43 -11
  76. package/dist/http-db.d.ts +13 -0
  77. package/dist/http-db.d.ts.map +1 -1
  78. package/dist/http-db.js +33 -4
  79. package/dist/indexeddb-db.d.ts.map +1 -1
  80. package/dist/indexeddb-db.js +65 -41
  81. package/dist/memory-db.d.ts.map +1 -1
  82. package/dist/memory-db.js +29 -8
  83. package/dist/studio/{KeyManagementPanel-DZuSeWBK.js → KeyManagementPanel-skUNnKRw.js} +36 -28
  84. package/dist/studio/app.d.ts +8 -1
  85. package/dist/studio/app.d.ts.map +1 -1
  86. package/dist/studio/components/KeyManagementPanel.d.ts.map +1 -1
  87. package/dist/studio/components/KeyManagementPanel.js +1 -1
  88. package/dist/studio/components/RouteErrorBoundary.d.ts +5 -0
  89. package/dist/studio/components/RouteErrorBoundary.d.ts.map +1 -0
  90. package/dist/studio/components/index.js +2 -2
  91. package/dist/studio/{components-Bivm2lX6.js → components-BSGmHfif.js} +6 -6
  92. package/dist/studio/control-plane/Agents.d.ts.map +1 -1
  93. package/dist/studio/control-plane/Data.d.ts.map +1 -1
  94. package/dist/studio/control-plane/Instance.d.ts +4 -1
  95. package/dist/studio/control-plane/Instance.d.ts.map +1 -1
  96. package/dist/studio/control-plane/Operations.d.ts +4 -1
  97. package/dist/studio/control-plane/Operations.d.ts.map +1 -1
  98. package/dist/studio/control-plane/Overview.d.ts.map +1 -1
  99. package/dist/studio/control-plane/Schema.d.ts.map +1 -1
  100. package/dist/studio/control-plane/context.d.ts +4 -1
  101. package/dist/studio/control-plane/context.d.ts.map +1 -1
  102. package/dist/studio/control-plane/index.js +2 -2
  103. package/dist/studio/control-plane/primitives.d.ts +24 -0
  104. package/dist/studio/control-plane/primitives.d.ts.map +1 -1
  105. package/dist/studio/{control-plane-CLm69XwX.js → control-plane-CaBA7Rgv.js} +1374 -1298
  106. package/dist/studio/index.d.ts +1 -0
  107. package/dist/studio/index.d.ts.map +1 -1
  108. package/dist/studio/index.js +415 -237
  109. package/dist/studio/navigation.d.ts +57 -0
  110. package/dist/studio/navigation.d.ts.map +1 -0
  111. package/dist/studio/studio.css +1 -1
  112. package/dist/studio/utils/control-plane-api.d.ts +2 -0
  113. package/dist/studio/utils/control-plane-api.d.ts.map +1 -1
  114. package/dist/studio/utils/control-plane-api.js +9 -4
  115. package/dist/studio/utils/service-api.d.ts.map +1 -1
  116. package/dist/studio/utils/service-api.js +5 -4
  117. package/dist/studio-app/assets/{feltdb_wasm-DACcD-zs.js → feltdb_wasm-3hDngl1h.js} +1 -1
  118. package/dist/studio-app/assets/feltdb_wasm_bg-CFQT4gCy.wasm +0 -0
  119. package/dist/studio-app/assets/index-Dql1hS5m.js +30 -0
  120. package/dist/studio-app/assets/{index-CiC8VY-a.css → index-vi21oEQa.css} +1 -1
  121. package/dist/studio-app/index.html +2 -2
  122. package/dist/telemetry.d.ts +1 -0
  123. package/dist/telemetry.d.ts.map +1 -1
  124. package/dist/telemetry.js +4 -3
  125. package/dist/transaction-identity.d.ts +62 -0
  126. package/dist/transaction-identity.d.ts.map +1 -0
  127. package/dist/transaction-identity.js +138 -0
  128. package/dist/transaction.d.ts +50 -5
  129. package/dist/transaction.d.ts.map +1 -1
  130. package/dist/transaction.js +28 -0
  131. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  132. package/dist/wasm/package.json +1 -1
  133. package/dist/workspace/local-development-authority.d.ts +19 -0
  134. package/dist/workspace/local-development-authority.d.ts.map +1 -1
  135. package/dist/workspace/local-development-authority.js +335 -15
  136. package/package.json +1 -1
  137. package/dist/create/server-source/crates/feltdb-wasm/test.db +0 -0
  138. package/dist/studio-app/assets/feltdb_wasm_bg-BejThWID.wasm +0 -0
  139. package/dist/studio-app/assets/index-CoZaxnlm.js +0 -30
@@ -5,7 +5,8 @@
5
5
  //! the socket boundary.
6
6
 
7
7
  use crate::{
8
- AtomicCommit, AtomicMutation, AtomicPrecondition, FeltDb, RecordPrecondition, StoredRow,
8
+ AtomicCommit, AtomicMutation, AtomicPrecondition, FeltDb, PayloadBinding, RecordPrecondition,
9
+ StoredRow,
9
10
  };
10
11
  use async_trait::async_trait;
11
12
  use serde::{Deserialize, Serialize};
@@ -120,8 +121,78 @@ impl EmbeddedAuthority {
120
121
  }
121
122
  }
122
123
 
124
+ /// Names the construction of the payload digest the authority binds ids to.
125
+ ///
126
+ /// The prefix is stored with the digest in the durable transaction record, so a
127
+ /// digest written by a different construction is recognizable as one this
128
+ /// build cannot judge, instead of being read as a mismatch.
129
+ const PAYLOAD_DIGEST_PREFIX: &str = "authority-tx1:sha256:";
130
+
131
+ /// Canonical text of a JSON value: object members sorted by name at every
132
+ /// depth, array order preserved. Written out here rather than left to
133
+ /// `serde_json`'s map ordering, which is a build feature that another crate in
134
+ /// the dependency graph can change.
135
+ fn canonical_json(value: &Value, out: &mut String) {
136
+ match value {
137
+ Value::Object(map) => {
138
+ let mut keys: Vec<&String> = map.keys().collect();
139
+ keys.sort();
140
+ out.push('{');
141
+ for (index, key) in keys.into_iter().enumerate() {
142
+ if index > 0 {
143
+ out.push(',');
144
+ }
145
+ out.push_str(&Value::String(key.clone()).to_string());
146
+ out.push(':');
147
+ canonical_json(&map[key], out);
148
+ }
149
+ out.push('}');
150
+ }
151
+ Value::Array(items) => {
152
+ out.push('[');
153
+ for (index, item) in items.iter().enumerate() {
154
+ if index > 0 {
155
+ out.push(',');
156
+ }
157
+ canonical_json(item, out);
158
+ }
159
+ out.push(']');
160
+ }
161
+ other => out.push_str(&other.to_string()),
162
+ }
163
+ }
164
+
165
+ /// The digest an id is bound to: everything the commit acts on or records, and
166
+ /// not the id itself. `audit` is included because it is committed evidence the
167
+ /// caller supplied, not transport metadata; two requests that record different
168
+ /// audit facts under one id are different requests.
169
+ fn payload_digest(transaction: &AuthorityTransaction) -> Result<String, AuthorityError> {
170
+ let semantic = serde_json::json!({
171
+ "base_revision": transaction.base_revision,
172
+ "preconditions": transaction.preconditions,
173
+ "record_preconditions": transaction.record_preconditions,
174
+ "mutations": transaction.mutations,
175
+ "audit": transaction.audit,
176
+ });
177
+ let mut canonical = String::new();
178
+ canonical_json(&semantic, &mut canonical);
179
+ Ok(format!(
180
+ "{PAYLOAD_DIGEST_PREFIX}{:x}",
181
+ Sha256::digest(canonical.as_bytes())
182
+ ))
183
+ }
184
+
123
185
  fn authority_error(error: crate::FlowError) -> AuthorityError {
124
186
  match error {
187
+ crate::FlowError::TransactionIdPayloadMismatch { transaction_id: id } => {
188
+ AuthorityError {
189
+ code: "IDEMPOTENCY_CONFLICT".into(),
190
+ message: format!(
191
+ "transaction {id} was already committed with a different payload"
192
+ ),
193
+ authoritative_revision: None,
194
+ }
195
+ }
125
196
  crate::FlowError::RevisionConflict {
126
197
  expected: _,
127
198
  actual,
@@ -152,17 +223,54 @@ impl FeltDbAuthority for EmbeddedAuthority {
152
223
  &self,
153
224
  transaction: AuthorityTransaction,
154
225
  ) -> Result<AtomicCommit, AuthorityError> {
155
- self.db
226
+ let digest = payload_digest(&transaction)?;
227
+ // How the id is already known, if it is. A recorded hash never changes
228
+ // and a missing one never appears, so this read cannot be invalidated
229
+ // by a concurrent commit: the comparison that matters -- the id against
230
+ // the digest of this request -- is made by the core, atomically with
231
+ // the commit, under the lock that serializes every writer.
232
+ let id = transaction.transaction_id.as_str();
233
+ let applied = self.db.has_applied_transaction(id).map_err(authority_error)?;
234
+ let recorded = if applied {
235
+ self.db
236
+ .applied_transaction_payload_hash(id)
237
+ .map_err(authority_error)?
238
+ } else {
239
+ None
240
+ };
241
+ let (bound, legacy) = match (applied, recorded) {
242
+ (false, _) => (Some(digest), false),
243
+ // Committed before ids were bound: the id is all there is to compare.
244
+ (true, None) => (None, true),
245
+ (true, Some(hash)) if hash.starts_with(PAYLOAD_DIGEST_PREFIX) => (Some(digest), false),
246
+ // Recorded by a construction this build does not know, or by another
247
+ // writer's: neither a match nor a mismatch can be established.
248
+ (true, Some(_)) => {
249
+ return Err(AuthorityError {
250
+ code: "IDEMPOTENCY_UNVERIFIABLE".into(),
251
+ message: format!(
252
+ "transaction {id} exists, but its recorded payload identity cannot be verified by this build"
253
+ ),
254
+ authoritative_revision: None,
255
+ })
256
+ }
257
+ };
258
+ let mut commit = self
259
+ .db
156
260
  .apply_atomic_transaction_guarded(
157
- &transaction.transaction_id,
158
- None,
261
+ id,
262
+ bound.as_deref(),
159
263
  transaction.base_revision,
160
264
  &transaction.preconditions,
161
265
  &transaction.record_preconditions,
162
266
  &transaction.mutations,
163
- transaction.audit,
267
+ transaction.audit.clone(),
164
268
  )
165
- .map_err(authority_error)
269
+ .map_err(authority_error)?;
270
+ if legacy && commit.duplicate {
271
+ commit.payload_binding = Some(PayloadBinding::LegacyUnverified);
272
+ }
273
+ Ok(commit)
166
274
  }
167
275
  }
168
276
 
@@ -267,6 +375,17 @@ impl AuthorityClient {
267
375
  }
268
376
 
269
377
  async fn request(&self, request: Request) -> Result<Response, AuthorityError> {
378
+ // Once a commit has been handed to the authority it may have been
379
+ // applied whether or not an answer comes back, so every failure from
380
+ // that point is an unknown outcome, not a failure.
381
+ let is_commit = matches!(request, Request::Commit { .. });
382
+ let unknown = |detail: String| AuthorityError {
383
+ code: "COMMIT_OUTCOME_UNKNOWN".into(),
384
+ message: format!(
385
+ "the commit may or may not have been applied ({detail}); retry with the same transaction id and payload"
386
+ ),
387
+ authoritative_revision: None,
388
+ };
270
389
  let mut stream = UnixStream::connect(&self.socket_path)
271
390
  .await
272
391
  .map_err(|error| AuthorityError {
@@ -283,17 +402,26 @@ impl AuthorityClient {
283
402
  })?,
284
403
  )
285
404
  .await
286
- .map_err(io_authority_error)?;
287
- stream.write_all(b"\n").await.map_err(io_authority_error)?;
405
+ .map_err(|error| if is_commit { unknown(error.to_string()) } else { io_authority_error(error) })?;
406
+ stream
407
+ .write_all(b"\n")
408
+ .await
409
+ .map_err(|error| if is_commit { unknown(error.to_string()) } else { io_authority_error(error) })?;
288
410
  let mut response = String::new();
289
411
  BufReader::new(stream)
290
412
  .read_line(&mut response)
291
413
  .await
292
- .map_err(io_authority_error)?;
293
- match serde_json::from_str(&response).map_err(|error| AuthorityError {
294
- code: "INVALID_RESPONSE".into(),
295
- message: error.to_string(),
296
- authoritative_revision: None,
414
+ .map_err(|error| if is_commit { unknown(error.to_string()) } else { io_authority_error(error) })?;
415
+ match serde_json::from_str(&response).map_err(|error| {
416
+ if is_commit {
417
+ unknown(format!("unreadable response: {error}"))
418
+ } else {
419
+ AuthorityError {
420
+ code: "INVALID_RESPONSE".into(),
421
+ message: error.to_string(),
422
+ authoritative_revision: None,
423
+ }
424
+ }
297
425
  })? {
298
426
  Response::Error(error) => Err(error),
299
427
  response => Ok(response),
@@ -340,8 +468,10 @@ impl FeltDbAuthority for AuthorityClient {
340
468
  match self.request(Request::Commit { transaction }).await? {
341
469
  Response::Commit(commit) => Ok(commit),
342
470
  _ => Err(AuthorityError {
343
- code: "INVALID_RESPONSE".into(),
344
- message: "expected authority commit".into(),
471
+ code: "COMMIT_OUTCOME_UNKNOWN".into(),
472
+ message: "the authority answered a commit with something other than a commit; \
473
+ its outcome is unknown, retry with the same transaction id and payload"
474
+ .into(),
345
475
  authoritative_revision: None,
346
476
  }),
347
477
  }
@@ -1,10 +1,158 @@
1
+ //! # FeltDB
2
+ //!
3
+ //! A durable, embedded state store. Records live under a *capability* (the part of a key before
4
+ //! the first `:`, so `"orders:o-1"` is record `o-1` of capability `orders`). Every write goes
5
+ //! through a durable log; several records can be changed in one atomic transaction; an update can
6
+ //! be made conditional on the version you read; and a transaction can carry an identity so that a
7
+ //! retry after an uncertain outcome is recognized instead of applied twice.
8
+ //!
9
+ //! ```
10
+ //! use feltdb::{AtomicMutation, FeltDb, FlowError, PayloadBinding, RecordPrecondition};
11
+ //! use serde_json::{json, Value};
12
+ //!
13
+ //! # fn main() -> Result<(), Box<dyn std::error::Error>> {
14
+ //! let dir = tempfile::tempdir()?;
15
+ //! let path = dir.path().join("app.db");
16
+ //! let db = FeltDb::open(&path)?;
17
+ //!
18
+ //! // Create a record only if it is absent: a second create is refused, and refused in a way a
19
+ //! // caller can branch on.
20
+ //! db.insert_if_absent("orders:o-1", json!({ "status": "pending" }))?;
21
+ //! assert!(matches!(
22
+ //! db.insert_if_absent("orders:o-1", json!({})),
23
+ //! Err(FlowError::PreconditionFailed(_))
24
+ //! ));
25
+ //!
26
+ //! // Change several records atomically, and name the transaction so a retry is safe. The payload
27
+ //! // hash is yours to compute: FeltDB stores it and compares it for equality.
28
+ //! let ship = |hash: &str| {
29
+ //! db.apply_atomic_transaction_content_addressed(
30
+ //! "ship-o-1",
31
+ //! Some(hash),
32
+ //! None,
33
+ //! &[],
34
+ //! &[
35
+ //! AtomicMutation {
36
+ //! capability: "orders".into(),
37
+ //! key: "orders:o-1".into(),
38
+ //! value: Some(json!({ "status": "shipped" })),
39
+ //! },
40
+ //! AtomicMutation {
41
+ //! capability: "audit".into(),
42
+ //! key: "audit:a-1".into(),
43
+ //! value: Some(json!({ "order": "o-1", "event": "shipped" })),
44
+ //! },
45
+ //! ],
46
+ //! None,
47
+ //! )
48
+ //! };
49
+ //! let first = ship("sha256:payload-v1")?;
50
+ //! assert!(!first.duplicate);
51
+ //!
52
+ //! // The same request again is recognized as the original, not applied a second time.
53
+ //! let retry = ship("sha256:payload-v1")?;
54
+ //! assert!(retry.duplicate);
55
+ //! assert_eq!(retry.payload_binding, Some(PayloadBinding::Verified));
56
+ //! assert_eq!(retry.commit_revision, first.commit_revision);
57
+ //!
58
+ //! // The same id with a different payload is refused, with a typed error. Nothing is written.
59
+ //! assert!(matches!(
60
+ //! ship("sha256:payload-v2"),
61
+ //! Err(FlowError::TransactionIdPayloadMismatch { .. })
62
+ //! ));
63
+ //!
64
+ //! // Close and reopen: what was acknowledged is there.
65
+ //! drop(db);
66
+ //! let db = FeltDb::open(&path)?;
67
+ //! assert_eq!(db.get::<Value>("orders:o-1")?, Some(json!({ "status": "shipped" })));
68
+ //! # let _ = RecordPrecondition::default();
69
+ //! # Ok(())
70
+ //! # }
71
+ //! ```
72
+ //!
73
+ //! ## What is supported
74
+ //!
75
+ //! Only the items in this table are covered by the compatibility policy
76
+ //! ([`docs/reference/feltdb-rust-api.md`] in the repository). While the version is `0.x`, a breaking
77
+ //! change to them is a minor-version bump and is recorded in the changelog; everything else may
78
+ //! change in any release.
79
+ //!
80
+ //! | To | Use |
81
+ //! | --- | --- |
82
+ //! | open, recover, reopen | [`FeltDb::open`], [`FeltDb::log_recovery`], [`FeltDb::health`] |
83
+ //! | read | [`FeltDb::get`], [`FeltDb::query`] |
84
+ //! | write unconditionally | [`FeltDb::insert`], [`FeltDb::update`], [`FeltDb::delete`] (all three are plain writes: see below) |
85
+ //! | create only if absent | [`FeltDb::insert_if_absent`] |
86
+ //! | update only if unchanged | [`FeltDb::compare_and_set_json`] → [`JsonCasResult`] |
87
+ //! | change several records atomically | [`FeltDb::apply_atomic_transaction`], [`FeltDb::apply_atomic_transaction_guarded`] with [`AtomicMutation`] and [`RecordPrecondition`] → [`AtomicCommit`] |
88
+ //! | make a retry safe | [`FeltDb::apply_atomic_transaction_content_addressed`], [`FeltDb::has_applied_transaction`], [`FeltDb::applied_transaction_payload_hash`], [`PayloadBinding`] |
89
+ //! | tell failures apart | [`FlowError`] and its variants, [`PreconditionFailure`] |
90
+ //! | a durable authority for several processes | the [`authority`] module (Unix) |
91
+ //!
92
+ //! **Behavior to know before relying on it.**
93
+ //! `insert`, `update` and `delete` are unconditional: `insert` replaces an existing record, `update`
94
+ //! of a missing key *creates* it, and `delete` of a missing key succeeds. A fenced write
95
+ //! ([`FeltDb::compare_and_set_json`], a [`RecordPrecondition`] with `expected_version`) treats a record
96
+ //! that has never been through one as version 1, and a record's `__version` becomes visible in its JSON
97
+ //! after its first fenced write. A transaction refused for a failed precondition writes nothing.
98
+ //! A transaction id committed *without* a payload hash can be retried (it is a duplicate, with no
99
+ //! [`PayloadBinding`]), but retrying it *with* a hash is refused, because there is nothing to verify against.
100
+ //!
101
+ //! ## Durability
102
+ //!
103
+ //! An acknowledged write survives the process being killed. Whether it survives the machine losing
104
+ //! power depends on the operating system and drive honouring a flush, and is **not** established.
105
+ //! A log ending in an incomplete append is recovered by discarding that append, which was never
106
+ //! acknowledged: [`FeltDb::log_recovery`] says so. A corrupt log, or one in a format this build does
107
+ //! not understand, refuses to open and is left as found.
108
+ //!
109
+ //! ## Stability tiers
110
+ //!
111
+ //! * **Supported**: the table above.
112
+ //! * **Experimental**: [`agent_state`], [`agent_transition_contract`], the deterministic-contract
113
+ //! adapters, [`semantic_decision`] and [`semantic_query`]. Public so they can be used, but they
114
+ //! may change shape before they are stabilized.
115
+ //! * **Not yet classified**: the other public modules. They are public today and carry no promise;
116
+ //! each is listed with the decision still needed in the repository's Rust API audit.
117
+ //! * **Internals**: implementation, fault-injection and diagnostic modules are private unless the
118
+ //! `unstable-internals` feature is enabled, and nothing behind that feature is covered by any promise.
119
+ //!
120
+ //! `FlowError` is `#[non_exhaustive]`: match on its variants, always with a catch-all arm, and never
121
+ //! on message text.
122
+ //!
123
+ //! [`docs/reference/feltdb-rust-api.md`]: https://github.com/rkendel1/flow_db/blob/main/docs/reference/feltdb-rust-api.md
124
+
125
+ macro_rules! unstable_internal_modules {
126
+ ($($name:ident),* $(,)?) => {
127
+ $(
128
+ #[cfg(feature = "unstable-internals")]
129
+ pub mod $name;
130
+ #[cfg(not(feature = "unstable-internals"))]
131
+ #[allow(dead_code, unused_imports)]
132
+ mod $name;
133
+ )*
134
+ };
135
+ }
136
+
137
+ unstable_internal_modules! {
138
+ adversarial_transport,
139
+ concurrency_fuzzing,
140
+ crash_injection,
141
+ replay_fuzzing,
142
+ permutation_scheduler,
143
+ persistence_reality,
144
+ in_process_transport,
145
+ query_execution_diagnostics,
146
+ workload_diagnostics,
147
+ cardinality_endpoint,
148
+ }
149
+
1
150
  #[cfg(test)]
2
151
  mod acceptance_tests;
3
152
  mod acquisition;
4
153
  pub mod admission;
5
154
  #[cfg(test)]
6
155
  mod admission_contract_tests;
7
- pub mod adversarial_transport;
8
156
  pub mod analytics;
9
157
  pub mod application;
10
158
  pub mod application_runtime;
@@ -17,33 +165,32 @@ mod authorization_security_tests;
17
165
  pub mod capabilities;
18
166
  mod capability;
19
167
  mod cardinality_diagnostics;
20
- pub mod cardinality_endpoint;
21
- pub mod causal_backlog_bound;
168
+ #[cfg(test)]
169
+ mod causal_backlog_bound;
22
170
  pub mod causal_dependency_barrier;
23
171
  #[cfg(test)]
24
172
  mod causal_dependency_barrier_phase_7_1;
25
- pub mod concurrency_fuzzing;
26
173
  pub mod consistency_contract;
27
174
  mod content_distribution;
28
175
  pub mod convergence;
29
176
  pub mod crash_atomic_boundary;
30
- pub mod crash_injection;
31
177
  #[cfg(test)]
32
178
  mod crash_recovery_tests;
33
179
  mod cron;
34
- pub mod dedup_bound_investigation;
180
+ #[cfg(test)]
181
+ mod dedup_bound_investigation;
35
182
  pub mod distributed_indexing;
36
183
  #[cfg(test)]
37
184
  mod distributed_tests;
38
185
  pub mod distributed_transactions;
39
186
  pub mod durability_guarantees;
40
187
  pub mod durable_dedup_set;
41
- pub mod durable_operation_identity;
188
+ #[cfg(test)]
189
+ mod durable_operation_identity;
42
190
  pub mod durable_operation_log;
43
191
  pub mod durable_sync;
44
192
  pub mod equality_index;
45
193
  mod execution;
46
- pub mod in_process_transport;
47
194
  pub mod indexing;
48
195
  #[cfg(test)]
49
196
  mod managed_cas_tests;
@@ -55,12 +202,12 @@ pub mod observability;
55
202
  mod operation;
56
203
  pub mod operation_algebra;
57
204
  pub mod operation_log;
58
- pub mod p1_application_atomicity;
59
- pub mod p1_atomicity_acceptance;
205
+ #[cfg(test)]
206
+ mod p1_application_atomicity;
207
+ #[cfg(test)]
208
+ mod p1_atomicity_acceptance;
60
209
  pub mod partition_reconciliation;
61
210
  mod peer_registry;
62
- pub mod permutation_scheduler;
63
- pub mod persistence_reality;
64
211
  #[cfg(test)]
65
212
  mod phase1b_acceptance;
66
213
  #[cfg(test)]
@@ -99,11 +246,10 @@ mod phase_5_soak;
99
246
  pub mod policy_evaluation;
100
247
  pub mod production_api;
101
248
  mod provenance;
102
- pub mod query_execution_diagnostics;
103
249
  pub mod query_performance;
104
- pub mod read_basis;
250
+ #[cfg(test)]
251
+ mod read_basis;
105
252
  mod references;
106
- pub mod replay_fuzzing;
107
253
  pub mod replica_acknowledgements;
108
254
  pub mod replica_membership;
109
255
  pub mod replication_manager;
@@ -112,6 +258,8 @@ mod routing;
112
258
  pub mod semantic_decision;
113
259
  pub mod semantic_query;
114
260
  pub mod sharding;
261
+ pub mod agent_state;
262
+ pub mod agent_transition_contract;
115
263
  pub mod state_conflict_contract;
116
264
  pub mod state_contract;
117
265
  pub mod state_diff_contract;
@@ -129,7 +277,8 @@ pub mod tcp_transport;
129
277
  pub mod transaction_api;
130
278
  #[cfg(test)]
131
279
  mod transaction_invariants;
132
- pub mod transaction_preconditions;
280
+ #[cfg(test)]
281
+ mod transaction_preconditions;
133
282
  pub mod transactions;
134
283
  mod trigger;
135
284
  pub mod trigger_contract;
@@ -141,13 +290,13 @@ mod workflow_acceptance_tests;
141
290
  mod workflow_integration;
142
291
  mod workflow_runtime;
143
292
  pub mod workload;
144
- pub mod workload_diagnostics;
145
293
 
146
294
  pub use acquisition::{
147
295
  AcquisitionDeduplicator, AcquisitionPolicy, AcquisitionResult, ContentRequest, ContentResponse,
148
296
  ContentTransferError,
149
297
  };
150
298
  pub use admission::{ProductionRetryPolicy, RejectionReason, SubmissionOutcome};
299
+ #[cfg(feature = "unstable-internals")]
151
300
  pub use adversarial_transport::{
152
301
  AdversarialTransport, ConnectionId, ConnectivityMatrix, FaultSeed, InMemoryTransport,
153
302
  LinkState, MessageId, NetworkEvent, NetworkSchedule, NodeId, ReplicationTransport,
@@ -210,7 +359,9 @@ pub use partition_reconciliation::{
210
359
  ReconciliationResult, ReplicaPartition,
211
360
  };
212
361
  pub use peer_registry::PeerRegistry;
362
+ #[cfg(feature = "unstable-internals")]
213
363
  pub use permutation_scheduler::{OperationSchedule, PermutationScheduler, ScheduleStrategy};
364
+ #[cfg(feature = "unstable-internals")]
214
365
  pub use persistence_reality::{
215
366
  CorruptionDetectionResult, DurablePersistenceLayer, FsyncDurabilityResult,
216
367
  PersistenceContractVerifier, PersistenceCrashPoint, RecoveryResult, WALEntry, WriteResult,
@@ -325,7 +476,14 @@ use tokio::sync::broadcast;
325
476
 
326
477
  pub type Result<T> = std::result::Result<T, FlowError>;
327
478
 
479
+ /// Why an operation failed.
480
+ ///
481
+ /// Branch on the variant, never on message text. The enum is `#[non_exhaustive]`: a later release
482
+ /// may add variants, so a `match` needs a catch-all arm. `CapabilityError` carries a human-readable
483
+ /// message, some of which begin with an upper-case code (`INVALID_READ_SET:`, `EMPTY_PRECONDITION:`);
484
+ /// those prefixes are **not** a supported way to tell failures apart and may change.
328
485
  #[derive(Debug)]
486
+ #[non_exhaustive]
329
487
  pub enum FlowError {
330
488
  Io(std::io::Error),
331
489
  Serde(serde_json::Error),
@@ -337,6 +495,16 @@ pub enum FlowError {
337
495
  /// outcome of a race and a caller is meant to branch on it, while a
338
496
  /// capability error is not.
339
497
  PreconditionFailed(Box<PreconditionFailure>),
498
+ /// A transaction id was already committed with a **different** payload. Nothing was written, and
499
+ /// the id still names the original transaction.
500
+ ///
501
+ /// Returned when a payload hash is supplied for an id that was committed with another one, and
502
+ /// also when an id that was committed *without* a hash is retried *with* one, since there is
503
+ /// nothing recorded to verify against. Retrying with the same id cannot succeed: use a new id
504
+ /// for a new transaction, or resend the original payload to confirm the original.
505
+ TransactionIdPayloadMismatch {
506
+ transaction_id: String,
507
+ },
340
508
  /// The transaction was based on a revision which is no longer current.
341
509
  RevisionConflict {
342
510
  expected: u64,
@@ -360,6 +528,10 @@ impl Display for FlowError {
360
528
  FlowError::CorruptLogLine(corruption) => write!(f, "CORRUPT_DURABLE_LOG: {corruption}"),
361
529
  FlowError::CapabilityError(msg) => write!(f, "capability error: {msg}"),
362
530
  FlowError::PreconditionFailed(failure) => write!(f, "PRECONDITION_FAILED: {failure}"),
531
+ FlowError::TransactionIdPayloadMismatch { transaction_id } => write!(
532
+ f,
533
+ "IDEMPOTENCY_CONFLICT: transaction {transaction_id} was already committed with a different payload"
534
+ ),
363
535
  FlowError::RevisionConflict { expected, actual } => write!(
364
536
  f,
365
537
  "REVISION_CONFLICT: expected authority revision {expected}, current revision is {actual}"
@@ -1012,6 +1184,23 @@ pub struct AtomicCommit {
1012
1184
  pub state_after: u64,
1013
1185
  pub rows: Vec<StoredRow>,
1014
1186
  pub duplicate: bool,
1187
+ /// How a duplicate was recognized. Present only on a duplicate, and only
1188
+ /// when the caller's request could be compared with what was committed
1189
+ /// (`Verified`) or was deliberately judged by id alone (`LegacyUnverified`).
1190
+ #[serde(default, skip_serializing_if = "Option::is_none")]
1191
+ pub payload_binding: Option<PayloadBinding>,
1192
+ }
1193
+
1194
+ /// What a duplicate transaction id was checked against.
1195
+ #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1196
+ #[serde(rename_all = "snake_case")]
1197
+ pub enum PayloadBinding {
1198
+ /// The id and the payload hash both match what was committed: this request
1199
+ /// is the original one.
1200
+ Verified,
1201
+ /// The id was committed without a payload hash, so only the id could be
1202
+ /// checked. The caller must not read this as proof the payload matches.
1203
+ LegacyUnverified,
1015
1204
  }
1016
1205
 
1017
1206
  #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@@ -3204,9 +3393,9 @@ impl FeltDb {
3204
3393
  .map(String::as_str)
3205
3394
  != Some(expected_hash)
3206
3395
  {
3207
- return Err(FlowError::CapabilityError(format!(
3208
- "TRANSACTION_ID_PAYLOAD_MISMATCH:{transaction_id}"
3209
- )));
3396
+ return Err(FlowError::TransactionIdPayloadMismatch {
3397
+ transaction_id: transaction_id.to_string(),
3398
+ });
3210
3399
  }
3211
3400
  }
3212
3401
  let (base_revision, commit_revision) = inner
@@ -3229,6 +3418,7 @@ impl FeltDb {
3229
3418
  state_after,
3230
3419
  rows: vec![],
3231
3420
  duplicate: true,
3421
+ payload_binding: payload_hash.map(|_| PayloadBinding::Verified),
3232
3422
  });
3233
3423
  }
3234
3424
  // Record preconditions, evaluated before anything is staged and
@@ -3599,6 +3789,7 @@ impl FeltDb {
3599
3789
  state_after: inner.sequence,
3600
3790
  rows,
3601
3791
  duplicate: false,
3792
+ payload_binding: None,
3602
3793
  },
3603
3794
  events,
3604
3795
  )
@@ -3962,6 +4153,15 @@ impl FeltDb {
3962
4153
  Ok(inner.sync_state.clone())
3963
4154
  }
3964
4155
 
4156
+ /// The authority's wall-clock time in milliseconds.
4157
+ ///
4158
+ /// The same reading the commit-time lease predicate uses, exposed so a
4159
+ /// decision that depends on lease expiry can be made against the clock that
4160
+ /// will later judge the commit. A caller's own clock is never a substitute.
4161
+ pub fn authority_time_ms(&self) -> u64 {
4162
+ now_ms() as u64
4163
+ }
4164
+
3965
4165
  /// Read an untyped value for protocol adapters such as the HTTP server.
3966
4166
  pub fn get_value(&self, key: &str) -> Result<Option<Value>> {
3967
4167
  self.get::<Value>(key)
@@ -6116,9 +6316,13 @@ mod tests {
6116
6316
  None,
6117
6317
  )
6118
6318
  .unwrap_err();
6119
- assert!(conflict
6120
- .to_string()
6121
- .contains("TRANSACTION_ID_PAYLOAD_MISMATCH:"));
6319
+ assert!(
6320
+ matches!(
6321
+ &conflict,
6322
+ FlowError::TransactionIdPayloadMismatch { transaction_id } if transaction_id == "managed-idempotency-1"
6323
+ ),
6324
+ "expected the typed idempotency conflict, got {conflict:?}"
6325
+ );
6122
6326
  let _ = fs::remove_file(sync_metadata_path(&path));
6123
6327
  let _ = fs::remove_file(path);
6124
6328
  }
@@ -1603,11 +1603,7 @@ pub(crate) mod durable {
1603
1603
  | PreconditionFailure::Version { .. } => Refusal::Changed,
1604
1604
  other => Refusal::Failed(format!("{other:?}")),
1605
1605
  },
1606
- FlowError::CapabilityError(message)
1607
- if message.starts_with("TRANSACTION_ID_PAYLOAD_MISMATCH") =>
1608
- {
1609
- Refusal::OtherRequest
1610
- }
1606
+ FlowError::TransactionIdPayloadMismatch { .. } => Refusal::OtherRequest,
1611
1607
  other => Refusal::Failed(other.to_string()),
1612
1608
  }
1613
1609
  }