@feltdb/core 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.
Files changed (83) hide show
  1. package/README.md +32 -0
  2. package/dist/agent-registry.js +1 -3
  3. package/dist/agent-runtime.js +8 -7
  4. package/dist/analytics-backend.js +3 -1
  5. package/dist/application-contract.js +1 -0
  6. package/dist/application-manifest.js +1 -0
  7. package/dist/artifact.js +2 -0
  8. package/dist/authorization.js +2 -0
  9. package/dist/bundle.js +2 -0
  10. package/dist/capability.js +1 -3
  11. package/dist/cell.js +9 -4
  12. package/dist/cli/commands.js +26 -2
  13. package/dist/cli/index.js +1 -1
  14. package/dist/collection.js +39 -31
  15. package/dist/create/package-versions.js +1 -1
  16. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +613 -27
  17. package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +452 -0
  18. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +208 -0
  19. package/dist/create/server-source/crates/feltdb/src/convergence.rs +16 -0
  20. package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +402 -0
  21. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +784 -24
  22. package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +418 -0
  23. package/dist/create/server-source/crates/feltdb/src/lib.rs +284 -0
  24. package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +471 -0
  25. package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +661 -0
  26. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +72 -3
  27. package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +899 -0
  28. package/dist/create/server-source/crates/feltdb-server/src/main.rs +116 -4
  29. package/dist/db.d.ts +2 -2
  30. package/dist/db.d.ts.map +1 -1
  31. package/dist/db.js +48 -15
  32. package/dist/development-runtime-bridge.js +1 -1
  33. package/dist/distributed-indexing.js +7 -5
  34. package/dist/embedded-transaction.d.ts +9 -0
  35. package/dist/embedded-transaction.d.ts.map +1 -1
  36. package/dist/embedded-transaction.js +95 -3
  37. package/dist/feltdb.d.ts +16 -0
  38. package/dist/feltdb.d.ts.map +1 -1
  39. package/dist/file-db.d.ts.map +1 -1
  40. package/dist/file-db.js +9 -3
  41. package/dist/flowspec.js +2 -1
  42. package/dist/http-client.js +2 -0
  43. package/dist/http-db.d.ts +11 -0
  44. package/dist/http-db.d.ts.map +1 -1
  45. package/dist/http-db.js +32 -3
  46. package/dist/identity.js +1 -0
  47. package/dist/index-analytics.js +6 -7
  48. package/dist/index-backend.js +3 -3
  49. package/dist/index-dashboard.js +10 -13
  50. package/dist/index-manager.js +12 -11
  51. package/dist/index-monitoring.js +9 -4
  52. package/dist/index-store.js +2 -0
  53. package/dist/indexeddb-db.d.ts.map +1 -1
  54. package/dist/indexeddb-db.js +32 -23
  55. package/dist/memory-db.d.ts.map +1 -1
  56. package/dist/memory-db.js +8 -4
  57. package/dist/observe.js +2 -0
  58. package/dist/provider.js +2 -0
  59. package/dist/query-planner.js +2 -4
  60. package/dist/reactive-graph.js +6 -8
  61. package/dist/release.js +2 -0
  62. package/dist/sharding.js +11 -6
  63. package/dist/state-contract.js +3 -3
  64. package/dist/studio-app/assets/{feltdb_wasm-CJv3wHzi.js → feltdb_wasm-CD744e5D.js} +1 -1
  65. package/dist/studio-app/assets/feltdb_wasm_bg-CiIXhOLi.wasm +0 -0
  66. package/dist/studio-app/assets/index-DoROs8yx.js +28 -0
  67. package/dist/studio-app/index.html +1 -1
  68. package/dist/sync-contract.js +9 -2
  69. package/dist/telemetry.d.ts.map +1 -1
  70. package/dist/telemetry.js +32 -12
  71. package/dist/transaction.d.ts +127 -9
  72. package/dist/transaction.d.ts.map +1 -1
  73. package/dist/transaction.js +91 -5
  74. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  75. package/dist/worker.js +2 -0
  76. package/dist/workload.js +2 -0
  77. package/dist/workspace/development-node.js +11 -10
  78. package/dist/workspace/investigation-lifecycle-manager.js +2 -0
  79. package/dist/workspace/investigation-supervisor.js +5 -3
  80. package/dist/workspace/workspace-connection.js +14 -5
  81. package/package.json +1 -1
  82. package/dist/studio-app/assets/feltdb_wasm_bg-C8TG8r2n.wasm +0 -0
  83. package/dist/studio-app/assets/index-DospFFYE.js +0 -28
@@ -24,6 +24,7 @@ pub mod analytics;
24
24
  pub mod distributed_indexing;
25
25
  pub mod indexing;
26
26
  pub mod sharding;
27
+ pub mod transaction_preconditions;
27
28
  pub mod transactions;
28
29
  pub mod state_hash;
29
30
  pub mod crash_injection;
@@ -43,11 +44,16 @@ pub mod crash_atomic_boundary;
43
44
  pub mod multi_operation_transaction;
44
45
  pub mod p1_atomicity_acceptance;
45
46
  pub mod p1_application_atomicity;
47
+ pub mod causal_backlog_bound;
48
+ pub mod dedup_bound_investigation;
49
+ pub mod durable_operation_identity;
46
50
  pub mod causal_dependency_barrier;
47
51
  pub mod partition_reconciliation;
48
52
  pub mod consistency_contract;
49
53
  pub mod persistence_reality;
50
54
  pub mod adversarial_transport;
55
+ pub mod replica_membership;
56
+ pub mod replica_acknowledgements;
51
57
  pub mod replication_manager;
52
58
  pub mod metrics;
53
59
  pub mod query_performance;
@@ -250,6 +256,12 @@ pub enum FlowError {
250
256
  Serde(serde_json::Error),
251
257
  CorruptLogLine(String),
252
258
  CapabilityError(String),
259
+ /// A transaction precondition did not hold. Nothing was written.
260
+ ///
261
+ /// Separate from `CapabilityError` because a conflict is an expected
262
+ /// outcome of a race and a caller is meant to branch on it, while a
263
+ /// capability error is not.
264
+ PreconditionFailed(Box<PreconditionFailure>),
253
265
  }
254
266
 
255
267
  impl Display for FlowError {
@@ -259,6 +271,7 @@ impl Display for FlowError {
259
271
  FlowError::Serde(e) => write!(f, "serde error: {e}"),
260
272
  FlowError::CorruptLogLine(line) => write!(f, "corrupt log line: {line}"),
261
273
  FlowError::CapabilityError(msg) => write!(f, "capability error: {msg}"),
274
+ FlowError::PreconditionFailed(failure) => write!(f, "PRECONDITION_FAILED: {failure}"),
262
275
  }
263
276
  }
264
277
  }
@@ -326,6 +339,109 @@ pub struct AtomicPrecondition {
326
339
  pub key: String,
327
340
  pub expected_version: Option<u64>,
328
341
  }
342
+
343
+ /// What must be true of one record for a transaction to commit.
344
+ ///
345
+ /// Distinct from `AtomicPrecondition`, and deliberately so. That one compares
346
+ /// the row's internal operation *sequence*, and `None` on it means "this record
347
+ /// must not exist". This one compares the fields a caller can actually read
348
+ /// back -- the document's `__version`, its authority epoch, its lease -- which
349
+ /// are the same fields single-record CAS compares. Plumbing a caller's
350
+ /// `expectedVersion` into the sequence check would have compared a document
351
+ /// version against a storage sequence: two different numbers that happen to
352
+ /// share a name.
353
+ ///
354
+ /// Every predicate is optional and only a supplied one is checked. That differs
355
+ /// from CAS, where an absent `expected_lease_id` asserts the record is *not*
356
+ /// leased; here an absent predicate asserts nothing at all. The difference is
357
+ /// deliberate -- a transaction precondition is a fence the caller opts into,
358
+ /// not a full description of the record -- and it is why the fields are named
359
+ /// `expected_*` rather than describing a state.
360
+ #[derive(Debug, Clone, Default, Serialize, Deserialize)]
361
+ pub struct RecordPrecondition {
362
+ pub capability: String,
363
+ pub key: String,
364
+ /// The record must not exist. Mutually exclusive with the predicates below.
365
+ #[serde(default)]
366
+ pub require_absent: bool,
367
+ /// The document's `__version`, as `updateIfVersion` compares it.
368
+ #[serde(default)]
369
+ pub expected_version: Option<u64>,
370
+ /// The record's authority epoch, at `/authority/epoch`.
371
+ #[serde(default)]
372
+ pub expected_epoch: Option<u64>,
373
+ /// The id of an unexpired lease held on the record.
374
+ #[serde(default)]
375
+ pub expected_lease_id: Option<String>,
376
+ }
377
+
378
+ impl RecordPrecondition {
379
+ /// True when this precondition asks for nothing, which is a caller error
380
+ /// rather than a satisfied fence: a precondition that constrains nothing
381
+ /// would read as protection and provide none.
382
+ pub fn is_empty(&self) -> bool {
383
+ !self.require_absent
384
+ && self.expected_version.is_none()
385
+ && self.expected_epoch.is_none()
386
+ && self.expected_lease_id.is_none()
387
+ }
388
+ }
389
+
390
+ /// Which predicate failed, and what the authority actually holds.
391
+ ///
392
+ /// Structured rather than a formatted string, because the caller has to tell a
393
+ /// lost race from a broken deployment: the first is retried with fresh state,
394
+ /// the second is not retried at all.
395
+ #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
396
+ #[serde(rename_all = "camelCase", tag = "predicate")]
397
+ pub enum PreconditionFailure {
398
+ /// Required absent, but the record exists.
399
+ Present { collection: String, key: String },
400
+ /// A predicate was given for a record that does not exist.
401
+ Missing { collection: String, key: String },
402
+ Version { collection: String, key: String, expected: u64, actual: u64 },
403
+ Epoch { collection: String, key: String, expected: u64, actual: u64 },
404
+ Lease { collection: String, key: String, expected: String, actual: Option<String> },
405
+ }
406
+
407
+ impl PreconditionFailure {
408
+ pub fn collection(&self) -> &str {
409
+ match self {
410
+ Self::Present { collection, .. }
411
+ | Self::Missing { collection, .. }
412
+ | Self::Version { collection, .. }
413
+ | Self::Epoch { collection, .. }
414
+ | Self::Lease { collection, .. } => collection,
415
+ }
416
+ }
417
+ /// The storage key, which is namespaced as `collection:id`.
418
+ pub fn key(&self) -> &str {
419
+ match self {
420
+ Self::Present { key, .. }
421
+ | Self::Missing { key, .. }
422
+ | Self::Version { key, .. }
423
+ | Self::Epoch { key, .. }
424
+ | Self::Lease { key, .. } => key,
425
+ }
426
+ }
427
+ }
428
+
429
+ impl Display for PreconditionFailure {
430
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
431
+ match self {
432
+ Self::Present { collection, key } =>
433
+ write!(f, "{collection}/{key} was required to be absent but exists"),
434
+ Self::Missing { collection, key } =>
435
+ write!(f, "{collection}/{key} does not exist"),
436
+ Self::Version { collection, key, expected, actual } =>
437
+ write!(f, "{collection}/{key} is at version {actual}, expected {expected}"),
438
+ Self::Epoch { collection, key, expected, actual } =>
439
+ write!(f, "{collection}/{key} is at epoch {actual}, expected {expected}"),
440
+ Self::Lease { collection, key, expected, actual } =>
441
+ write!(f, "{collection}/{key} holds lease {actual:?}, expected {expected}"),
442
+ }
443
+ }
444
+ }
329
445
  #[derive(Debug, Clone, Serialize, Deserialize)]
330
446
  pub struct AtomicCommit {
331
447
  pub transaction_id: String,
@@ -927,6 +1043,40 @@ impl FeltDb {
927
1043
  preconditions: &[AtomicPrecondition],
928
1044
  mutations: &[AtomicMutation],
929
1045
  audit: Option<Value>,
1046
+ ) -> Result<AtomicCommit> {
1047
+ self.apply_atomic_transaction_guarded(
1048
+ transaction_id,
1049
+ payload_hash,
1050
+ expected_parent,
1051
+ preconditions,
1052
+ &[],
1053
+ mutations,
1054
+ audit,
1055
+ )
1056
+ }
1057
+
1058
+ /// Commit a transaction only if every record precondition holds.
1059
+ ///
1060
+ /// The preconditions are evaluated inside the same lock as the writes and
1061
+ /// before any of them, so there is no state in which one record is checked
1062
+ /// while the others commit. A failure returns
1063
+ /// `FlowError::PreconditionFailed` and applies nothing -- not a prefix, not
1064
+ /// the guarded record, nothing.
1065
+ ///
1066
+ /// A failed precondition does **not** consume the transaction id. The id is
1067
+ /// recorded only after a commit, so a caller that loses a race may retry
1068
+ /// the same id once it has re-read state, and a caller that wants the
1069
+ /// retry to be a distinct decision may use a new one. Both work; the choice
1070
+ /// is the caller's and is asserted in the tests rather than left implied.
1071
+ pub fn apply_atomic_transaction_guarded(
1072
+ &self,
1073
+ transaction_id: &str,
1074
+ payload_hash: Option<&str>,
1075
+ expected_parent: Option<u64>,
1076
+ preconditions: &[AtomicPrecondition],
1077
+ record_preconditions: &[RecordPrecondition],
1078
+ mutations: &[AtomicMutation],
1079
+ audit: Option<Value>,
930
1080
  ) -> Result<AtomicCommit> {
931
1081
  let (commit, events) = {
932
1082
  let mut inner = self.inner.lock().expect("lock poisoned");
@@ -944,6 +1094,140 @@ impl FeltDb {
944
1094
  duplicate: true,
945
1095
  });
946
1096
  }
1097
+ // Record preconditions, evaluated before anything is staged and
1098
+ // under the same lock the writes take. A caller reads state,
1099
+ // decides, and commits; this is the fence that makes the decision
1100
+ // still true at the moment of the write.
1101
+ for condition in record_preconditions {
1102
+ if condition.is_empty() {
1103
+ return Err(FlowError::CapabilityError(format!(
1104
+ "EMPTY_PRECONDITION:{}:{}",
1105
+ condition.capability, condition.key
1106
+ )));
1107
+ }
1108
+ let collection = condition.capability.clone();
1109
+ let key = condition.key.clone();
1110
+ let current = inner
1111
+ .rows
1112
+ .get(&condition.capability)
1113
+ .and_then(|bucket| bucket.get(&condition.key));
1114
+
1115
+ if condition.require_absent {
1116
+ if current.is_some() {
1117
+ return Err(FlowError::PreconditionFailed(Box::new(
1118
+ PreconditionFailure::Present { collection, key },
1119
+ )));
1120
+ }
1121
+ continue;
1122
+ }
1123
+
1124
+ let Some(current) = current else {
1125
+ return Err(FlowError::PreconditionFailed(Box::new(
1126
+ PreconditionFailure::Missing { collection, key },
1127
+ )));
1128
+ };
1129
+
1130
+ // The same fields single-record CAS compares, read the same way.
1131
+ if let Some(expected) = condition.expected_version {
1132
+ let actual = current
1133
+ .value
1134
+ .get("__version")
1135
+ .and_then(Value::as_u64)
1136
+ .unwrap_or(1);
1137
+ if actual != expected {
1138
+ return Err(FlowError::PreconditionFailed(Box::new(
1139
+ PreconditionFailure::Version { collection, key, expected, actual },
1140
+ )));
1141
+ }
1142
+ }
1143
+ if let Some(expected) = condition.expected_epoch {
1144
+ let actual = current
1145
+ .value
1146
+ .pointer("/authority/epoch")
1147
+ .and_then(Value::as_u64)
1148
+ .unwrap_or(0);
1149
+ if actual != expected {
1150
+ return Err(FlowError::PreconditionFailed(Box::new(
1151
+ PreconditionFailure::Epoch { collection, key, expected, actual },
1152
+ )));
1153
+ }
1154
+ }
1155
+ if let Some(expected) = &condition.expected_lease_id {
1156
+ let lease = current.value.get("lease").filter(|lease| !lease.is_null());
1157
+ let held = lease.and_then(|lease| {
1158
+ lease.get("leaseId").and_then(Value::as_str).map(str::to_string)
1159
+ });
1160
+ // An expired lease is not held, so naming it is a conflict:
1161
+ // the caller believes it owns something it no longer does.
1162
+ let unexpired = lease
1163
+ .and_then(|lease| lease.get("expiresAt").and_then(Value::as_u64))
1164
+ .is_some_and(|expires_at| expires_at > now_ms() as u64);
1165
+ if held.as_deref() != Some(expected.as_str()) || !unexpired {
1166
+ return Err(FlowError::PreconditionFailed(Box::new(
1167
+ PreconditionFailure::Lease {
1168
+ collection,
1169
+ key,
1170
+ expected: expected.clone(),
1171
+ actual: held,
1172
+ },
1173
+ )));
1174
+ }
1175
+ }
1176
+ }
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.
1183
+ //
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
1188
+ // docs/architecture/transaction-version-contract.md.
1189
+ let mut advanced: Vec<AtomicMutation> = Vec::new();
1190
+ let fenced: HashMap<(&str, &str), u64> = record_preconditions
1191
+ .iter()
1192
+ .filter_map(|condition| {
1193
+ condition
1194
+ .expected_version
1195
+ .map(|version| ((condition.capability.as_str(), condition.key.as_str()), version))
1196
+ })
1197
+ .collect();
1198
+ let mutations: &[AtomicMutation] = if fenced.is_empty() {
1199
+ mutations
1200
+ } else {
1201
+ advanced = mutations
1202
+ .iter()
1203
+ .map(|mutation| {
1204
+ let Some(expected) =
1205
+ fenced.get(&(mutation.capability.as_str(), mutation.key.as_str()))
1206
+ else {
1207
+ return mutation.clone();
1208
+ };
1209
+ // A delete has no record left to carry a version, so a
1210
+ // fenced delete is fence-then-remove and nothing more.
1211
+ let Some(Value::Object(fields)) = mutation.value.clone() else {
1212
+ return mutation.clone();
1213
+ };
1214
+ 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));
1221
+ AtomicMutation {
1222
+ capability: mutation.capability.clone(),
1223
+ key: mutation.key.clone(),
1224
+ value: Some(Value::Object(fields)),
1225
+ }
1226
+ })
1227
+ .collect();
1228
+ &advanced
1229
+ };
1230
+
947
1231
  if let Some(expected) = expected_parent {
948
1232
  if expected != inner.sequence {
949
1233
  return Err(FlowError::CapabilityError(format!(