@feltdb/core 0.7.3 → 0.8.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 (147) hide show
  1. package/README.md +25 -0
  2. package/dist/application-development.d.ts +78 -0
  3. package/dist/application-development.d.ts.map +1 -0
  4. package/dist/application-development.js +153 -0
  5. package/dist/application-manifest.d.ts +6 -1
  6. package/dist/application-manifest.d.ts.map +1 -1
  7. package/dist/application-manifest.js +10 -3
  8. package/dist/canonical-application.d.ts +27 -0
  9. package/dist/canonical-application.d.ts.map +1 -0
  10. package/dist/canonical-application.js +46 -0
  11. package/dist/cli/ai-model-config.js +97 -0
  12. package/dist/cli/cli.js +1 -1
  13. package/dist/cli/commands.js +664 -120
  14. package/dist/cli/index.js +1 -1
  15. package/dist/cli/managed-environment.js +6 -0
  16. package/dist/cli/managed-project.js +6 -0
  17. package/dist/cli/proposal-client.js +63 -0
  18. package/dist/cli/publish-engine.js +165 -0
  19. package/dist/cli/source-sync.js +183 -0
  20. package/dist/create/cli.js +11 -2
  21. package/dist/create/create.js +38 -16
  22. package/dist/create/managed-account.js +82 -11
  23. package/dist/create/package-versions.js +1 -1
  24. package/dist/create/server-source/crates/feltdb/src/application.rs +41 -1
  25. package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +84 -6
  26. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +126 -38
  27. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +72 -34
  28. package/dist/create/server-source/crates/feltdb/src/lib.rs +20 -2
  29. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +12 -24
  30. package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +11 -3
  31. package/dist/create/server-source/crates/feltdb-server/src/authenticated_principal.rs +0 -1
  32. package/dist/create/server-source/crates/feltdb-server/src/certification_harness.rs +23 -22
  33. package/dist/create/server-source/crates/feltdb-server/src/delegation_token.rs +5 -15
  34. package/dist/create/server-source/crates/feltdb-server/src/durable_operations.rs +13 -28
  35. package/dist/create/server-source/crates/feltdb-server/src/identity.rs +111 -3
  36. package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +28 -7
  37. package/dist/create/server-source/crates/feltdb-server/src/lib.rs +5 -5
  38. package/dist/create/server-source/crates/feltdb-server/src/main.rs +1960 -143
  39. package/dist/create/server-source/crates/feltdb-server/src/managed_diagnostics.rs +10 -30
  40. package/dist/create/server-source/crates/feltdb-server/src/membership_policy.rs +12 -4
  41. package/dist/create/server-source/crates/feltdb-server/src/request_telemetry.rs +8 -6
  42. package/dist/create/server-source/crates/feltdb-server/src/snapshot_cursor.rs +10 -14
  43. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +195 -13
  44. package/dist/create/server-source/crates/feltdb-server/src/tenant_policies.rs +9 -21
  45. package/dist/create/server-source/crates/feltdb-server/src/transaction_idempotency.rs +5 -3
  46. package/dist/create/server-source/crates/feltdb-server/src/transaction_recovery.rs +9 -8
  47. package/dist/create/server-source/crates/feltdb-server/tests/revision_recovery_integration_test.rs +1 -4
  48. package/dist/create/template/default-project/README.md +57 -0
  49. package/dist/create/template/default-project/agents/activity-assistant.ts +18 -0
  50. package/dist/create/template/default-project/agents/project-assistant.ts +19 -0
  51. package/dist/create/template/default-project/capabilities/activity-summary.ts +14 -0
  52. package/dist/create/template/default-project/capabilities/project-search.ts +16 -0
  53. package/dist/create/template/default-project/capabilities/project-summary.ts +15 -0
  54. package/dist/create/template/default-project/feltdb.flow +174 -0
  55. package/dist/create/template/default-project/src/App.tsx +20 -0
  56. package/dist/create/template/default-project/src/context/AuthContext.tsx +27 -0
  57. package/dist/create/template/default-project/src/feltdb.ts +87 -0
  58. package/dist/create/template/default-project/src/index.tsx +14 -0
  59. package/dist/create/template/default-project/src/pages/Activity.tsx +7 -0
  60. package/dist/create/template/default-project/src/pages/Agents.tsx +15 -0
  61. package/dist/create/template/default-project/src/pages/Dashboard.tsx +17 -0
  62. package/dist/create/template/default-project/src/pages/Invitations.tsx +11 -0
  63. package/dist/create/template/default-project/src/pages/Projects.tsx +11 -0
  64. package/dist/create/template/default-project/src/pages/SignIn.tsx +8 -0
  65. package/dist/create/template/default-project/src/pages/SignUp.tsx +8 -0
  66. package/dist/create/template/default-project/src/styles-application.css +21 -0
  67. package/dist/create/template/default-project/src/styles.css +1 -0
  68. package/dist/create/template/default-project/workflows/agent-assisted-summary.ts +1 -0
  69. package/dist/create/template/default-project/workflows/invitation.ts +30 -0
  70. package/dist/create/template/default-project/workflows/project-created.ts +2 -0
  71. package/dist/db.d.ts +49 -1
  72. package/dist/db.d.ts.map +1 -1
  73. package/dist/db.js +77 -0
  74. package/dist/error-codes.d.ts +20 -1
  75. package/dist/error-codes.d.ts.map +1 -1
  76. package/dist/error-codes.js +25 -0
  77. package/dist/flowspec.d.ts +1 -0
  78. package/dist/flowspec.d.ts.map +1 -1
  79. package/dist/flowspec.js +66 -11
  80. package/dist/http-client.d.ts +135 -14
  81. package/dist/http-client.d.ts.map +1 -1
  82. package/dist/http-client.js +79 -31
  83. package/dist/http-db.d.ts +15 -2
  84. package/dist/http-db.d.ts.map +1 -1
  85. package/dist/http-db.js +113 -5
  86. package/dist/index-core.d.ts +5 -0
  87. package/dist/index-core.d.ts.map +1 -1
  88. package/dist/index-core.js +5 -0
  89. package/dist/module-intent.d.ts +37 -0
  90. package/dist/module-intent.d.ts.map +1 -0
  91. package/dist/module-intent.js +81 -0
  92. package/dist/module.d.ts +76 -0
  93. package/dist/module.d.ts.map +1 -0
  94. package/dist/module.js +61 -0
  95. package/dist/proposal.d.ts +161 -0
  96. package/dist/proposal.d.ts.map +1 -0
  97. package/dist/proposal.js +232 -0
  98. package/dist/studio/app.d.ts +5 -1
  99. package/dist/studio/app.d.ts.map +1 -1
  100. package/dist/studio/components/ApplicationDesigner.d.ts +2 -1
  101. package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
  102. package/dist/studio/components/AskFeltDB.d.ts +21 -0
  103. package/dist/studio/components/AskFeltDB.d.ts.map +1 -0
  104. package/dist/studio/components/ContractInspector.d.ts +7 -0
  105. package/dist/studio/components/ContractInspector.d.ts.map +1 -0
  106. package/dist/studio/components/ManagedInstancePanel.d.ts.map +1 -1
  107. package/dist/studio/components/ManagedInstancePanel.js +1 -5
  108. package/dist/studio/components/ModuleExplorer.d.ts +6 -0
  109. package/dist/studio/components/ModuleExplorer.d.ts.map +1 -0
  110. package/dist/studio/components/ProposalBrowser.d.ts +12 -0
  111. package/dist/studio/components/ProposalBrowser.d.ts.map +1 -0
  112. package/dist/studio/components/StateExplorer.d.ts +6 -7
  113. package/dist/studio/components/StateExplorer.d.ts.map +1 -1
  114. package/dist/studio/components/index.d.ts +4 -0
  115. package/dist/studio/components/index.d.ts.map +1 -1
  116. package/dist/studio/components/index.js +4 -4
  117. package/dist/studio/components-CNSNrmA9.js +2787 -0
  118. package/dist/studio/index.js +214 -157
  119. package/dist/studio/studio.css +1 -1
  120. package/dist/studio/utils/index.d.ts +2 -0
  121. package/dist/studio/utils/index.d.ts.map +1 -1
  122. package/dist/studio/utils/index.js +14 -12
  123. package/dist/studio/utils/managed-instance.d.ts +0 -4
  124. package/dist/studio/utils/managed-instance.d.ts.map +1 -1
  125. package/dist/studio/utils/managed-instance.js +0 -4
  126. package/dist/studio/utils/proposal-api.d.ts +53 -0
  127. package/dist/studio/utils/proposal-api.d.ts.map +1 -0
  128. package/dist/studio/utils/proposal-api.js +91 -0
  129. package/dist/studio/utils/service-api.d.ts +81 -0
  130. package/dist/studio/utils/service-api.d.ts.map +1 -0
  131. package/dist/studio/utils/service-api.js +51 -0
  132. package/dist/studio-app/assets/dist-CCOk39Uc.js +1 -0
  133. package/dist/studio-app/assets/{feltdb_wasm-DVKsw75S.js → feltdb_wasm-CSB6KVgw.js} +1 -1
  134. package/dist/studio-app/assets/feltdb_wasm_bg-Dd1fnO9U.wasm +0 -0
  135. package/dist/studio-app/assets/index-Bncji3aN.js +43 -0
  136. package/dist/studio-app/assets/{index-C1GWyazR.css → index-yazaWMUN.css} +1 -1
  137. package/dist/studio-app/assets/lib-B2_7fcn7.js +69 -0
  138. package/dist/studio-app/assets/worker-CARZ5g7K.js +69 -0
  139. package/dist/studio-app/index.html +2 -2
  140. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  141. package/dist/workspace/local-development-authority.d.ts +3 -0
  142. package/dist/workspace/local-development-authority.d.ts.map +1 -1
  143. package/dist/workspace/local-development-authority.js +124 -2
  144. package/package.json +1 -1
  145. package/dist/studio/components-Dxuhrv8_.js +0 -1832
  146. package/dist/studio-app/assets/feltdb_wasm_bg-DNyNf0yy.wasm +0 -0
  147. package/dist/studio-app/assets/index-B5tnmvSD.js +0 -28
@@ -51,9 +51,11 @@ use feltdb::distributed_transactions::{
51
51
  CausalCapacity, DistributedTransactionExecutor, ReceiveOutcome, ReplicationMessage,
52
52
  SubmitOutcome,
53
53
  };
54
- use feltdb::replication_protocol::ProtocolTransport;
55
- use feltdb::replica_acknowledgements::{AcknowledgementStore, ReclamationEvidence, ReplicaAcknowledgement};
54
+ use feltdb::replica_acknowledgements::{
55
+ AcknowledgementStore, ReclamationEvidence, ReplicaAcknowledgement,
56
+ };
56
57
  use feltdb::replica_membership::MembershipStore;
58
+ use feltdb::replication_protocol::ProtocolTransport;
57
59
  use feltdb::state_hash::StateHash;
58
60
  use feltdb::tcp_transport::TcpTransport;
59
61
  use feltdb::transactions::{
@@ -127,7 +129,11 @@ fn deliver(
127
129
  Ok(ReceiveOutcome::AlreadyKnown) => {
128
130
  json!({ "envelope": key, "outcome": "already_known" })
129
131
  }
130
- Ok(ReceiveOutcome::CapacityDeferred { missing, pending_entries, pending_bytes }) => {
132
+ Ok(ReceiveOutcome::CapacityDeferred {
133
+ missing,
134
+ pending_entries,
135
+ pending_bytes,
136
+ }) => {
131
137
  // Durable, but not buffered. The caller's obligation is to stop
132
138
  // reading from this peer; the envelope is admitted from the log
133
139
  // once the backlog drains.
@@ -178,16 +184,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
178
184
  let mut max_pending_entries: Option<usize> = None;
179
185
  let mut max_pending_bytes: Option<usize> = None;
180
186
  let mut authority_priority: u32 = 0;
187
+ let mut heartbeat_interval_ms: u64 = 1_000;
188
+ let mut heartbeat_failure_threshold: u32 = 5;
181
189
 
182
190
  let mut index = 1;
183
191
  while index < args.len() {
184
192
  match args[index].as_str() {
185
193
  "--node-id" => {
186
194
  index += 1;
187
- node_id = args
188
- .get(index)
189
- .ok_or("--node-id requires a value")?
190
- .clone();
195
+ node_id = args.get(index).ok_or("--node-id requires a value")?.clone();
191
196
  }
192
197
  "--data" => {
193
198
  index += 1;
@@ -197,11 +202,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
197
202
  }
198
203
  "--send" => {
199
204
  index += 1;
200
- send_specs.push(parse_link(args.get(index).ok_or("--send requires PEER=ADDR")?)?);
205
+ send_specs.push(parse_link(
206
+ args.get(index).ok_or("--send requires PEER=ADDR")?,
207
+ )?);
201
208
  }
202
209
  "--recv" => {
203
210
  index += 1;
204
- recv_specs.push(parse_link(args.get(index).ok_or("--recv requires PEER=ADDR")?)?);
211
+ recv_specs.push(parse_link(
212
+ args.get(index).ok_or("--recv requires PEER=ADDR")?,
213
+ )?);
205
214
  }
206
215
  // The bound is settable so an acceptance test can reach it through
207
216
  // the production path. Filling the shipping default would mean
@@ -237,6 +246,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
237
246
  .parse::<u32>()
238
247
  .map_err(|e| format!("--authority-priority: {e}"))?;
239
248
  }
249
+ "--heartbeat-interval-ms" => {
250
+ index += 1;
251
+ heartbeat_interval_ms = args
252
+ .get(index)
253
+ .ok_or("--heartbeat-interval-ms requires a value")?
254
+ .parse::<u64>()
255
+ .map_err(|e| format!("--heartbeat-interval-ms: {e}"))?;
256
+ }
257
+ "--heartbeat-failure-threshold" => {
258
+ index += 1;
259
+ heartbeat_failure_threshold = args
260
+ .get(index)
261
+ .ok_or("--heartbeat-failure-threshold requires a value")?
262
+ .parse::<u32>()
263
+ .map_err(|e| format!("--heartbeat-failure-threshold: {e}"))?;
264
+ }
240
265
  other => return Err(format!("unknown argument {other}").into()),
241
266
  }
242
267
  index += 1;
@@ -282,7 +307,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
282
307
  // recover its previous role. The cluster ID is set by the first `authority-claim`
283
308
  // command, so it starts as the node's own ID.
284
309
  let authority = Arc::new(Mutex::new(
285
- AuthorityStore::open(
310
+ AuthorityStore::open_with_failure_detector(
286
311
  data_dir
287
312
  .as_ref()
288
313
  .map(|dir| dir.join("authority.json"))
@@ -290,13 +315,18 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
290
315
  &node_id, // cluster_id defaults to node_id until set
291
316
  &node_id,
292
317
  authority_priority,
318
+ heartbeat_interval_ms,
319
+ heartbeat_failure_threshold,
293
320
  )
294
321
  .map_err(|e| e.to_string())?,
295
322
  ));
296
323
 
297
324
  let initial_state = StateHash::from_hex("0".repeat(64));
298
- let mut executor =
299
- DistributedTransactionExecutor::with_log(node_id.clone(), initial_state.clone(), log_path.clone())?;
325
+ let mut executor = DistributedTransactionExecutor::with_log(
326
+ node_id.clone(),
327
+ initial_state.clone(),
328
+ log_path.clone(),
329
+ )?;
300
330
 
301
331
  // The bound is set before recovery, not after, because recovery restores
302
332
  // the backlog and has to respect the same limit the live path does.
@@ -338,7 +368,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
338
368
  // missed an operation observably divergent. It lives beside the executor
339
369
  // under one lock so a local commit and an arriving replication cannot
340
370
  // interleave and chain onto a parent that is already stale.
341
- let core = Arc::new(Mutex::new((executor, StateVersion::new(VectorClock::new(), "0".repeat(64)))));
371
+ let recovered_version = executor
372
+ .get_replica_state(&node_id)
373
+ .map(|replica| StateVersion::new(replica.vector_clock, replica.state_hash.to_string()))
374
+ .unwrap_or_else(|| StateVersion::new(VectorClock::new(), "0".repeat(64)));
375
+ let core = Arc::new(Mutex::new((executor, recovered_version)));
342
376
  let should_exit = Arc::new(AtomicBool::new(false));
343
377
  // Per-peer delivery hold.
344
378
  //
@@ -367,7 +401,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
367
401
  .into_iter()
368
402
  .map(|(peer, addr)| {
369
403
  let transport = TcpTransport::new("127.0.0.1:0".to_string());
370
- SendLink { peer, addr, control: Arc::new(Mutex::new(transport.clone())), io: transport }
404
+ SendLink {
405
+ peer,
406
+ addr,
407
+ control: Arc::new(Mutex::new(transport.clone())),
408
+ io: transport,
409
+ }
371
410
  })
372
411
  .collect();
373
412
 
@@ -375,7 +414,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
375
414
  .into_iter()
376
415
  .map(|(peer, addr)| {
377
416
  let transport = TcpTransport::new(addr.clone());
378
- RecvLink { peer, addr, control: Arc::new(Mutex::new(transport.clone())), io: transport }
417
+ RecvLink {
418
+ peer,
419
+ addr,
420
+ control: Arc::new(Mutex::new(transport.clone())),
421
+ io: transport,
422
+ }
379
423
  })
380
424
  .collect();
381
425
 
@@ -509,8 +553,35 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
509
553
  };
510
554
 
511
555
  match verb {
512
- "execute-tx" => {
513
- let record = if argument.is_empty() { "item".to_string() } else { argument };
556
+ "execute-tx" | "authority-write" => {
557
+ let record = if verb == "authority-write" {
558
+ let mut parts = argument.splitn(2, ' ');
559
+ let token = parts.next().and_then(|value| value.parse::<u64>().ok());
560
+ let record = parts.next().unwrap_or("item").to_string();
561
+ let validation = match token {
562
+ Some(token) => authority
563
+ .lock()
564
+ .await
565
+ .validate_quorum_write(FencingToken(token), 2),
566
+ None => Err(feltdb::authority_failover::AuthorityError::StaleToken {
567
+ provided: FencingToken(0),
568
+ current: authority.lock().await.state().current_token(),
569
+ }),
570
+ };
571
+ if let Err(error) = validation {
572
+ println!(
573
+ "AUTHORITY_WRITE_REJECTED {}",
574
+ json!({ "record": record, "reason": error.to_string() })
575
+ );
576
+ io::stdout().flush().ok();
577
+ continue;
578
+ }
579
+ record
580
+ } else if argument.is_empty() {
581
+ "item".to_string()
582
+ } else {
583
+ argument
584
+ };
514
585
 
515
586
  let mut fields = HashMap::new();
516
587
  fields.insert("record".to_string(), json!(record.clone()));
@@ -854,7 +925,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
854
925
  let (frontier, marks) = {
855
926
  let guard = core.lock().await;
856
927
  let (executor, _) = &*guard;
857
- (executor.causal_frontier().clone(), executor.origin_sequence_high_water())
928
+ (
929
+ executor.causal_frontier().clone(),
930
+ executor.origin_sequence_high_water(),
931
+ )
858
932
  };
859
933
  let view: Vec<String> = membership
860
934
  .get_members()
@@ -1083,27 +1157,30 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
1083
1157
  "list-operations" => {
1084
1158
  let guard = core.lock().await;
1085
1159
  let (executor, _) = &*guard;
1086
-
1160
+
1087
1161
  // Get all operations from the log
1088
1162
  let mut all_ops = Vec::new();
1089
1163
  let mut applied_ops = Vec::new();
1090
1164
  let pending_ops = executor.pending_causal_keys();
1091
1165
  let deferred_ops = executor.deferred_causal_keys();
1092
-
1166
+
1093
1167
  if let Some(ref log) = executor.operation_log {
1094
1168
  if let Ok(envelopes) = log.load_all() {
1095
1169
  for env in &envelopes {
1096
- let key = format!("{}:{}", env.envelope_id.originating_node, env.envelope_id.sequence);
1170
+ let key = format!(
1171
+ "{}:{}",
1172
+ env.envelope_id.originating_node, env.envelope_id.sequence
1173
+ );
1097
1174
  all_ops.push(key.clone());
1098
1175
  }
1099
1176
  }
1100
1177
  }
1101
-
1178
+
1102
1179
  // Get applied operations from the barrier
1103
1180
  for key in executor.causal_barrier.applied_keys() {
1104
1181
  applied_ops.push(key.to_string());
1105
1182
  }
1106
-
1183
+
1107
1184
  println!(
1108
1185
  "OPERATIONS {}",
1109
1186
  json!({
@@ -1120,23 +1197,26 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
1120
1197
  "operation-inventory" => {
1121
1198
  let guard = core.lock().await;
1122
1199
  let (executor, _) = &*guard;
1123
-
1200
+
1124
1201
  // Get all operations from the log
1125
1202
  let mut operations = json!({
1126
1203
  "applied": [],
1127
1204
  "pending": [],
1128
1205
  "deferred": [],
1129
1206
  });
1130
-
1207
+
1131
1208
  if let Some(ref log) = executor.operation_log {
1132
1209
  if let Ok(envelopes) = log.load_all() {
1133
1210
  let applied_keys = executor.causal_barrier.applied_keys();
1134
1211
  let pending_keys = executor.pending_causal_keys();
1135
1212
  let deferred_keys = executor.deferred_causal_keys();
1136
-
1213
+
1137
1214
  for env in &envelopes {
1138
- let key = format!("{}:{}", env.envelope_id.originating_node, env.envelope_id.sequence);
1139
-
1215
+ let key = format!(
1216
+ "{}:{}",
1217
+ env.envelope_id.originating_node, env.envelope_id.sequence
1218
+ );
1219
+
1140
1220
  if applied_keys.contains(&key) {
1141
1221
  operations["applied"].as_array_mut().unwrap().push(json!({
1142
1222
  "id": key,
@@ -1159,7 +1239,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
1159
1239
  }
1160
1240
  }
1161
1241
  }
1162
-
1242
+
1163
1243
  println!("INVENTORY {}", operations);
1164
1244
  io::stdout().flush().ok();
1165
1245
  }
@@ -1198,7 +1278,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
1198
1278
  })
1199
1279
  );
1200
1280
  }
1201
- Ok(ClaimResult::Rejected { current_authority, current_token }) => {
1281
+ Ok(ClaimResult::Rejected {
1282
+ current_authority,
1283
+ current_token,
1284
+ }) => {
1202
1285
  println!(
1203
1286
  "AUTHORITY_REJECTED {}",
1204
1287
  json!({
@@ -1230,11 +1313,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
1230
1313
  "authority-accept" => {
1231
1314
  let mut parts = argument.split_whitespace();
1232
1315
  let authority_node = parts.next().unwrap_or_default().to_string();
1233
- let token_value: u64 = parts
1234
- .next()
1235
- .unwrap_or("0")
1236
- .parse()
1237
- .unwrap_or(0);
1316
+ let token_value: u64 = parts.next().unwrap_or("0").parse().unwrap_or(0);
1238
1317
  let token = FencingToken(token_value);
1239
1318
 
1240
1319
  let mut guard = authority.lock().await;
@@ -1265,7 +1344,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
1265
1344
  let operations_applied = {
1266
1345
  let core_guard = core.lock().await;
1267
1346
  let (executor, _) = &*core_guard;
1268
- executor.get_replica_state(&node_id).map(|r| r.operations_applied).unwrap_or(0)
1347
+ executor
1348
+ .get_replica_state(&node_id)
1349
+ .map(|r| r.operations_applied)
1350
+ .unwrap_or(0)
1269
1351
  };
1270
1352
  println!(
1271
1353
  "AUTHORITY {}",
@@ -1291,7 +1373,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
1291
1373
  let from_node = parts.next().unwrap_or_default().to_string();
1292
1374
 
1293
1375
  if from_node.is_empty() {
1294
- println!("ERROR {}", json!({ "reason": "heartbeat requires NODE_ID" }));
1376
+ println!(
1377
+ "ERROR {}",
1378
+ json!({ "reason": "heartbeat requires NODE_ID" })
1379
+ );
1295
1380
  } else {
1296
1381
  let mut guard = authority.lock().await;
1297
1382
  guard.heartbeat(&from_node);
@@ -1377,7 +1462,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
1377
1462
  }
1378
1463
 
1379
1464
  _ => {
1380
- println!("ERROR {}", json!({ "reason": format!("unknown command {verb}") }));
1465
+ println!(
1466
+ "ERROR {}",
1467
+ json!({ "reason": format!("unknown command {verb}") })
1468
+ );
1381
1469
  io::stdout().flush().ok();
1382
1470
  }
1383
1471
  }
@@ -7,7 +7,6 @@
7
7
  /// - DistributedExecutor: Orchestrates consistency semantics
8
8
  ///
9
9
  /// Invariant: All replicas converge to identical state_hash for same operations
10
-
11
10
  use crate::causal_dependency_barrier::CausalDependencyBarrier;
12
11
  use crate::convergence::VectorClock;
13
12
  use crate::durable_operation_log::OperationLog;
@@ -136,11 +135,7 @@ impl ReplicaState {
136
135
  }
137
136
 
138
137
  /// Update replica state after transaction
139
- pub fn apply_transaction(
140
- &mut self,
141
- envelope: &TransactionEnvelope,
142
- new_state_hash: StateHash,
143
- ) {
138
+ pub fn apply_transaction(&mut self, envelope: &TransactionEnvelope, new_state_hash: StateHash) {
144
139
  self.vector_clock.merge(&envelope.vector_clock);
145
140
  self.vector_clock.increment(&self.node_id);
146
141
  self.state_hash = new_state_hash;
@@ -276,7 +271,10 @@ impl CausalCapacity {
276
271
 
277
272
  /// A bound small enough to be reached by a test through the real path.
278
273
  pub fn new(max_pending_entries: usize, max_pending_bytes: usize) -> Self {
279
- Self { max_pending_entries, max_pending_bytes }
274
+ Self {
275
+ max_pending_entries,
276
+ max_pending_bytes,
277
+ }
280
278
  }
281
279
  }
282
280
 
@@ -393,8 +391,12 @@ impl DistributedTransactionExecutor {
393
391
  /// source `next_origin_sequence` is recovered from.
394
392
  pub fn origin_sequence_high_water(&self) -> std::collections::BTreeMap<String, u64> {
395
393
  let mut marks = std::collections::BTreeMap::new();
396
- let Some(ref log) = self.operation_log else { return marks };
397
- let Ok(envelopes) = log.load_all() else { return marks };
394
+ let Some(ref log) = self.operation_log else {
395
+ return marks;
396
+ };
397
+ let Ok(envelopes) = log.load_all() else {
398
+ return marks;
399
+ };
398
400
  for envelope in envelopes {
399
401
  let entry = marks
400
402
  .entry(envelope.envelope_id.originating_node.clone())
@@ -516,7 +518,9 @@ impl DistributedTransactionExecutor {
516
518
 
517
519
  /// The size local work would contribute, measured before it is executed.
518
520
  fn operations_bytes(operations: &[Operation]) -> usize {
519
- serde_json::to_string(operations).map(|json| json.len()).unwrap_or(0)
521
+ serde_json::to_string(operations)
522
+ .map(|json| json.len())
523
+ .unwrap_or(0)
520
524
  }
521
525
 
522
526
  /// Execute local work unconditionally.
@@ -582,8 +586,10 @@ impl DistributedTransactionExecutor {
582
586
  // Step 6: A locally executed operation is applied, so it advances the
583
587
  // frontier exactly as a replicated one does. Own writes are always
584
588
  // causally eligible: they are built on the frontier they advance.
585
- self.causal_barrier
586
- .record_applied(&Self::barrier_key(&envelope.envelope_id), &envelope.vector_clock);
589
+ self.causal_barrier.record_applied(
590
+ &Self::barrier_key(&envelope.envelope_id),
591
+ &envelope.vector_clock,
592
+ );
587
593
 
588
594
  Ok(envelope)
589
595
  }
@@ -712,7 +718,9 @@ impl DistributedTransactionExecutor {
712
718
  /// per-operation constant, so a large envelope consumes its actual byte
713
719
  /// count.
714
720
  fn envelope_bytes(envelope: &TransactionEnvelope) -> usize {
715
- serde_json::to_string(envelope).map(|json| json.len()).unwrap_or(0)
721
+ serde_json::to_string(envelope)
722
+ .map(|json| json.len())
723
+ .unwrap_or(0)
716
724
  }
717
725
 
718
726
  /// Apply one envelope and advance the applied frontier.
@@ -741,8 +749,10 @@ impl DistributedTransactionExecutor {
741
749
 
742
750
  // The frontier advances here and only here, so it describes what has
743
751
  // been successfully applied rather than what was claimed or received.
744
- self.causal_barrier
745
- .record_applied(&Self::barrier_key(&envelope.envelope_id), &envelope.vector_clock);
752
+ self.causal_barrier.record_applied(
753
+ &Self::barrier_key(&envelope.envelope_id),
754
+ &envelope.vector_clock,
755
+ );
746
756
 
747
757
  Ok(state_hash)
748
758
  }
@@ -816,16 +826,14 @@ impl DistributedTransactionExecutor {
816
826
  return Ok(0);
817
827
  }
818
828
  let by_key = self.log_by_key()?;
819
- let mut keys: Vec<String> = self
820
- .deferred
821
- .iter()
822
- .map(Self::barrier_key)
823
- .collect();
829
+ let mut keys: Vec<String> = self.deferred.iter().map(Self::barrier_key).collect();
824
830
  keys.sort();
825
831
 
826
832
  let mut admitted = 0;
827
833
  for key in keys {
828
- let Some(envelope) = by_key.get(&key).cloned() else { continue };
834
+ let Some(envelope) = by_key.get(&key).cloned() else {
835
+ continue;
836
+ };
829
837
  let dependencies = Self::dependencies_of(&envelope);
830
838
  let payload_bytes = Self::envelope_bytes(&envelope);
831
839
 
@@ -888,9 +896,7 @@ impl DistributedTransactionExecutor {
888
896
  .clocks
889
897
  .iter()
890
898
  .filter(|(node, required)| frontier.get(node) < **required)
891
- .map(|(node, required)| {
892
- format!("{}@{} (have {})", node, required, frontier.get(node))
893
- })
899
+ .map(|(node, required)| format!("{}@{} (have {})", node, required, frontier.get(node)))
894
900
  .collect();
895
901
  missing.sort();
896
902
  missing
@@ -1021,11 +1027,7 @@ impl DistributedTransactionExecutor {
1021
1027
  return true;
1022
1028
  }
1023
1029
 
1024
- let first_hash = self
1025
- .replicas
1026
- .values()
1027
- .next()
1028
- .map(|r| r.state_hash.clone());
1030
+ let first_hash = self.replicas.values().next().map(|r| r.state_hash.clone());
1029
1031
 
1030
1032
  if let Some(first) = first_hash {
1031
1033
  self.replicas.values().all(|r| r.state_hash == first)
@@ -1122,6 +1124,27 @@ impl DistributedTransactionExecutor {
1122
1124
  );
1123
1125
  }
1124
1126
 
1127
+ // Restore the observable local replica state from the same applied
1128
+ // subset. Recovery previously rebuilt the barrier and canonical data
1129
+ // but left this counter and clock at their constructor values, making
1130
+ // a fully recovered node report that it had applied zero operations.
1131
+ let applied_keys = self.causal_barrier.applied_keys();
1132
+ let operations_applied = envelopes
1133
+ .iter()
1134
+ .filter(|envelope| applied_keys.contains(&Self::barrier_key(&envelope.envelope_id)))
1135
+ .map(|envelope| envelope.operations.len() as u64)
1136
+ .sum();
1137
+ let recovered_clock = self.causal_barrier.get_frontier().clone();
1138
+ let recovered_hash = self
1139
+ .canonical_state()
1140
+ .map(|state| state.hash())
1141
+ .unwrap_or_else(|_| StateHash::from_hex("0".repeat(64)));
1142
+ if let Some(local) = self.replicas.get_mut(&self.local_node_id) {
1143
+ local.vector_clock = recovered_clock;
1144
+ local.operations_applied = operations_applied;
1145
+ local.state_hash = recovered_hash;
1146
+ }
1147
+
1125
1148
  // Identity is recovered from the same log, and this is the half that
1126
1149
  // was missing: the causal frontier was already rebuilt here, so a
1127
1150
  // restarted origin came back knowing its causal position and not its
@@ -1201,7 +1224,10 @@ mod tests {
1201
1224
 
1202
1225
  #[test]
1203
1226
  fn test_replica_state_creation() {
1204
- let state = ReplicaState::new("node1".to_string(), StateHash::from_hex("hash0".to_string()));
1227
+ let state = ReplicaState::new(
1228
+ "node1".to_string(),
1229
+ StateHash::from_hex("hash0".to_string()),
1230
+ );
1205
1231
 
1206
1232
  assert_eq!(state.node_id, "node1");
1207
1233
  assert_eq!(state.operations_applied, 0);
@@ -1224,7 +1250,10 @@ mod tests {
1224
1250
  StateHash::from_hex("hash0".to_string()),
1225
1251
  );
1226
1252
 
1227
- executor.register_replica("node2".to_string(), StateHash::from_hex("hash0".to_string()));
1253
+ executor.register_replica(
1254
+ "node2".to_string(),
1255
+ StateHash::from_hex("hash0".to_string()),
1256
+ );
1228
1257
 
1229
1258
  assert!(executor.replicas.contains_key("node2"));
1230
1259
  }
@@ -1247,8 +1276,14 @@ mod tests {
1247
1276
  StateHash::from_hex("hash0".to_string()),
1248
1277
  );
1249
1278
 
1250
- executor.register_replica("node2".to_string(), StateHash::from_hex("hash0".to_string()));
1251
- executor.register_replica("node3".to_string(), StateHash::from_hex("hash0".to_string()));
1279
+ executor.register_replica(
1280
+ "node2".to_string(),
1281
+ StateHash::from_hex("hash0".to_string()),
1282
+ );
1283
+ executor.register_replica(
1284
+ "node3".to_string(),
1285
+ StateHash::from_hex("hash0".to_string()),
1286
+ );
1252
1287
 
1253
1288
  assert!(executor.check_convergence());
1254
1289
  }
@@ -1260,7 +1295,10 @@ mod tests {
1260
1295
  StateHash::from_hex("hash0".to_string()),
1261
1296
  );
1262
1297
 
1263
- executor.register_replica("node2".to_string(), StateHash::from_hex("hash1".to_string()));
1298
+ executor.register_replica(
1299
+ "node2".to_string(),
1300
+ StateHash::from_hex("hash1".to_string()),
1301
+ );
1264
1302
 
1265
1303
  assert!(!executor.check_convergence());
1266
1304
  }
@@ -246,7 +246,8 @@ use serde_json::Value;
246
246
  use sha2::{Digest, Sha256};
247
247
  use std::any::type_name;
248
248
  use std::collections::hash_map::DefaultHasher;
249
- use std::collections::{HashMap, HashSet};
249
+ use std::collections::{BTreeMap, HashMap, HashSet};
250
+ use std::ops::Bound::{Excluded, Unbounded};
250
251
  use std::fmt::{Display, Formatter};
251
252
  use std::fs::{self, OpenOptions};
252
253
  use std::hash::{Hash, Hasher};
@@ -315,7 +316,7 @@ pub struct FeltDb {
315
316
  #[allow(dead_code)] // Reserved runtime subsystems are initialized before their public APIs land.
316
317
  struct Inner {
317
318
  path: PathBuf,
318
- rows: HashMap<String, HashMap<String, StoredRow>>,
319
+ rows: HashMap<String, BTreeMap<String, StoredRow>>,
319
320
  key_counters: HashMap<String, u64>,
320
321
  query_count_by_type: HashMap<String, usize>,
321
322
  adaptive_indexes: HashSet<String>,
@@ -1721,6 +1722,23 @@ impl FeltDb {
1721
1722
  .unwrap_or_default())
1722
1723
  }
1723
1724
 
1725
+ /// Traverse one collection in immutable record-key order without materializing it.
1726
+ pub fn list_collection_page(&self, capability: &str, after: Option<&str>, limit: usize) -> Result<Vec<StoredRow>> {
1727
+ let inner = self.inner.lock().expect("lock poisoned");
1728
+ let Some(rows) = inner.rows.get(capability) else { return Ok(vec![]) };
1729
+ let values: Box<dyn Iterator<Item = &StoredRow>> = match after {
1730
+ Some(key) => Box::new(rows.range::<str, _>((Excluded(key), Unbounded)).map(|(_, row)| row)),
1731
+ None => Box::new(rows.values()),
1732
+ };
1733
+ Ok(values.filter(|row| !row.deleted).take(limit).cloned().collect())
1734
+ }
1735
+
1736
+ /// Fetch one live collection record without materializing collection state.
1737
+ pub fn get_collection_record(&self, capability: &str, key: &str) -> Result<Option<StoredRow>> {
1738
+ let inner = self.inner.lock().expect("lock poisoned");
1739
+ Ok(inner.rows.get(capability).and_then(|rows| rows.get(key)).filter(|row| !row.deleted).cloned())
1740
+ }
1741
+
1724
1742
  /// Subscribe to canonical mutation events.
1725
1743
  pub fn subscribe_changes(&self) -> broadcast::Receiver<ChangeEvent> {
1726
1744
  self.event_tx.subscribe()
@@ -1,10 +1,10 @@
1
+ use serde_json::Value;
1
2
  use std::path::PathBuf;
2
3
  use std::{
3
4
  collections::HashMap,
4
5
  sync::{atomic::AtomicU64, Arc},
5
6
  time::Instant,
6
7
  };
7
- use serde_json::Value;
8
8
 
9
9
  #[derive(Clone)]
10
10
  pub struct BoundedQueryCursor {
@@ -16,34 +16,22 @@ pub struct BoundedQueryCursor {
16
16
  pub created_at: u64,
17
17
  }
18
18
 
19
+ use crate::{
20
+ application_contract::ApplicationContractStore, artifacts::ArtifactStore, audit::AuditLog,
21
+ auth::KeyStore, causal::CausalStore, clock::LeaseClock, cluster::ClusterStore,
22
+ connections::ConnectionStore, content::ContentStore, identity::IdentityStore,
23
+ leases::LeaseStore, metrics::Metrics, portable_bundle::BundleStore, principals::PrincipalStore,
24
+ providers::ProviderStore, releases::ReleaseStore, sessions::SessionVerifier,
25
+ tenancy::TenancyStore,
26
+ };
19
27
  use feltdb::{
20
- FeltDb,
21
28
  application::ApplicationStore,
29
+ application_runtime::RuntimeStore,
22
30
  authorization::{GrantSigner, GrantStore},
23
31
  sync_contract::SyncStore,
24
- workload::WorkloadStore,
25
32
  worker_mesh::WorkerMeshStore,
26
- application_runtime::RuntimeStore,
27
- };
28
- use crate::{
29
- artifacts::ArtifactStore,
30
- audit::AuditLog,
31
- auth::KeyStore,
32
- causal::CausalStore,
33
- cluster::ClusterStore,
34
- connections::ConnectionStore,
35
- content::ContentStore,
36
- identity::IdentityStore,
37
- leases::LeaseStore,
38
- metrics::Metrics,
39
- portable_bundle::BundleStore,
40
- principals::PrincipalStore,
41
- providers::ProviderStore,
42
- releases::ReleaseStore,
43
- sessions::SessionVerifier,
44
- tenancy::TenancyStore,
45
- application_contract::ApplicationContractStore,
46
- clock::LeaseClock,
33
+ workload::WorkloadStore,
34
+ FeltDb,
47
35
  };
48
36
 
49
37
  #[derive(Clone)]