@feltdb/core 0.9.1 → 0.10.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.
- package/dist/cli/commands.js +19 -21
- package/dist/cli/development-session.js +16 -0
- package/dist/cli/studio-artifact.js +33 -0
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +8 -1
- package/dist/create/server-source/crates/feltdb/src/application.rs +16 -0
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +21 -22
- package/dist/create/server-source/crates/feltdb/src/lib.rs +451 -33
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +48 -3
- package/dist/create/server-source/crates/feltdb/src/state_model.rs +569 -4
- package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +1 -0
- package/dist/create/server-source/crates/feltdb/src/versioned_object_substrate.rs +708 -0
- package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +1 -0
- package/dist/create/server-source/crates/feltdb/src/workload.rs +480 -44
- package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_evidence.rs +123 -0
- package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_substrate_completion.rs +281 -0
- package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +15 -3
- package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +28 -18
- package/dist/create/server-source/crates/feltdb/tests/storage_independent_versioned_object_substrate.rs +328 -0
- package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +20 -3
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +14 -4
- package/dist/create/server-source/crates/feltdb-server/src/backup.rs +0 -4
- package/dist/create/server-source/crates/feltdb-server/src/canonical_principals.rs +1093 -0
- package/dist/create/server-source/crates/feltdb-server/src/causal.rs +905 -144
- package/dist/create/server-source/crates/feltdb-server/src/connections.rs +9 -6
- package/dist/create/server-source/crates/feltdb-server/src/control_plane.rs +90 -0
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +1 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +408 -74
- package/dist/create/server-source/crates/feltdb-server/src/principals.rs +9 -501
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +22 -0
- package/dist/create/server-source/crates/feltdb-server/tests/principal_tenant_migration.rs +247 -0
- package/dist/db.d.ts +6 -5
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +18 -24
- package/dist/deployment.d.ts.map +1 -1
- package/dist/deployment.js +4 -2
- package/dist/embedded-transaction.d.ts.map +1 -1
- package/dist/embedded-transaction.js +4 -0
- package/dist/error-codes.d.ts +12 -0
- package/dist/error-codes.d.ts.map +1 -1
- package/dist/error-codes.js +20 -2
- package/dist/feltdb.d.ts +4 -0
- package/dist/feltdb.d.ts.map +1 -1
- package/dist/file-db.d.ts +10 -0
- package/dist/file-db.d.ts.map +1 -1
- package/dist/file-db.js +18 -0
- package/dist/http-db.d.ts +4 -0
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +73 -12
- package/dist/index-core.d.ts +1 -0
- package/dist/index-core.d.ts.map +1 -1
- package/dist/index-core.js +1 -0
- package/dist/public-contract.d.ts +112 -0
- package/dist/public-contract.d.ts.map +1 -0
- package/dist/public-contract.js +125 -0
- package/dist/studio/app.d.ts +23 -0
- package/dist/studio/app.d.ts.map +1 -1
- package/dist/studio/components/ContractInspector.d.ts +5 -0
- package/dist/studio/components/ContractInspector.d.ts.map +1 -1
- package/dist/studio/components/SettingsPanel.d.ts +4 -1
- package/dist/studio/components/SettingsPanel.d.ts.map +1 -1
- package/dist/studio/components/index.js +4 -4
- package/dist/studio/{components-CehkcX_k.js → components-CJFgdKG6.js} +322 -303
- package/dist/studio/control-plane/Data.d.ts.map +1 -1
- package/dist/studio/control-plane/Instance.d.ts +10 -0
- package/dist/studio/control-plane/Instance.d.ts.map +1 -1
- package/dist/studio/control-plane/Operations.d.ts +3 -0
- package/dist/studio/control-plane/Operations.d.ts.map +1 -1
- package/dist/studio/control-plane/State.d.ts.map +1 -1
- package/dist/studio/control-plane/index.d.ts +4 -2
- package/dist/studio/control-plane/index.d.ts.map +1 -1
- package/dist/studio/control-plane/index.js +2 -2
- package/dist/studio/control-plane-DP8ljTDO.js +3207 -0
- package/dist/studio/index.js +96 -75
- package/dist/studio/utils/api.d.ts +1 -1
- package/dist/studio/utils/api.d.ts.map +1 -1
- package/dist/studio/utils/control-plane-api.d.ts +12 -1
- package/dist/studio/utils/control-plane-api.d.ts.map +1 -1
- package/dist/studio/utils/control-plane-api.js +1 -1
- package/dist/studio/utils/index.js +16 -16
- package/dist/studio/utils/service-api.d.ts +8 -0
- package/dist/studio/utils/service-api.d.ts.map +1 -1
- package/dist/studio/utils/service-api.js +15 -12
- package/dist/studio-app/assets/{feltdb_wasm-DksXDnLa.js → feltdb_wasm-CfNb3IoF.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-D9d3gY-E.wasm +0 -0
- package/dist/studio-app/assets/index-SDXdROi1.js +30 -0
- package/dist/studio-app/feltdb-studio.json +6 -0
- package/dist/studio-app/index.html +1 -1
- package/dist/transaction.d.ts +9 -1
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +6 -2
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workspace/local-development-authority.d.ts +8 -0
- package/dist/workspace/local-development-authority.d.ts.map +1 -1
- package/dist/workspace/local-development-authority.js +136 -1
- package/package.json +2 -2
- package/dist/studio/control-plane-D6TT2fCQ.js +0 -3064
- package/dist/studio-app/assets/feltdb_wasm_bg-Yy4QRHJT.wasm +0 -0
- package/dist/studio-app/assets/index-CZ55nLgp.js +0 -30
|
@@ -130,6 +130,7 @@ pub mod transaction_preconditions;
|
|
|
130
130
|
pub mod transactions;
|
|
131
131
|
mod trigger;
|
|
132
132
|
pub mod trigger_contract;
|
|
133
|
+
pub mod versioned_object_substrate;
|
|
133
134
|
pub mod worker_mesh;
|
|
134
135
|
mod workflow;
|
|
135
136
|
#[cfg(test)]
|
|
@@ -238,9 +239,11 @@ pub use state_facade::FeltDBStateSystem;
|
|
|
238
239
|
pub use state_hash::{CanonicalState, StateHash};
|
|
239
240
|
pub use state_model::{
|
|
240
241
|
apply_reconciliation_plan, path_relation, paths_overlap, reconcile, resolve_path, ChangeKind,
|
|
241
|
-
ConflictClass, ConflictClassification,
|
|
242
|
-
|
|
243
|
-
|
|
242
|
+
ConflictClass, ConflictClassification, GarbageCollectionError, GarbageCollectionReport,
|
|
243
|
+
MaterializationError, MaterializationProjection, PathComponent, PathConflict, PathRelation,
|
|
244
|
+
ReconciliationPlan, RefError, Relationship, SemanticChange, SemanticDiff, StateId, StateObject,
|
|
245
|
+
StateReconciliationResult, StateRef, StateRevision, StateStore, StateTopology,
|
|
246
|
+
STATE_MODEL_VERSION,
|
|
244
247
|
};
|
|
245
248
|
pub use state_transition_store::{MemoryStateTransitionStore, StateTransitionRecord};
|
|
246
249
|
pub use state_trigger::{
|
|
@@ -262,6 +265,10 @@ pub use trigger_contract::{
|
|
|
262
265
|
ContractRuntime, ContractTrigger, MatchOutcome, MatchReason, TriggerMatchInput,
|
|
263
266
|
TriggerMatchOutput, TRIGGER_MATCH_CONTRACT_ID, TRIGGER_MATCH_CONTRACT_VERSION,
|
|
264
267
|
};
|
|
268
|
+
pub use versioned_object_substrate::{
|
|
269
|
+
DurableObjectStore, DurableRef, LocalObjectStore, Reachability, S3CompatibleBucket,
|
|
270
|
+
S3CompatibleObjectStore, SubstrateError, VersionedObjectSubstrate,
|
|
271
|
+
};
|
|
265
272
|
pub use workflow::{
|
|
266
273
|
BlockedReason, WorkflowGraph, WorkflowInstance, WorkflowNode, WorkflowOperation, WorkflowRef,
|
|
267
274
|
WorkflowState, WorkflowStepRef, WorkflowStepState,
|
|
@@ -403,6 +410,10 @@ struct Inner {
|
|
|
403
410
|
/// audit archive, and the durable log remains the record of what happened.
|
|
404
411
|
/// Keeping it bounded is what makes it safe to maintain on the commit path.
|
|
405
412
|
transaction_attributions: VecDeque<TransactionAttribution>,
|
|
413
|
+
/// Complete committed transaction order, rebuilt exclusively from the
|
|
414
|
+
/// durable transaction log. Unlike the bounded operator window above this
|
|
415
|
+
/// is the canonical replay/provenance index.
|
|
416
|
+
transaction_history: Vec<TransactionAttribution>,
|
|
406
417
|
collection_cardinality: HashMap<String, u64>,
|
|
407
418
|
/// Derived equality index over `rows`. Never durable, never replicated, and
|
|
408
419
|
/// never consulted for anything but candidate selection: it is maintained
|
|
@@ -505,6 +516,12 @@ impl Inner {
|
|
|
505
516
|
let bucket = self.rows.entry(capability.clone()).or_default();
|
|
506
517
|
let previous = bucket.insert(key.clone(), row);
|
|
507
518
|
drop(mutation);
|
|
519
|
+
if previous.is_none() {
|
|
520
|
+
*self
|
|
521
|
+
.collection_cardinality
|
|
522
|
+
.entry(capability.clone())
|
|
523
|
+
.or_default() += 1;
|
|
524
|
+
}
|
|
508
525
|
if !indexed {
|
|
509
526
|
return previous.is_none();
|
|
510
527
|
}
|
|
@@ -543,6 +560,11 @@ impl Inner {
|
|
|
543
560
|
return false;
|
|
544
561
|
};
|
|
545
562
|
drop(mutation);
|
|
563
|
+
let count = self
|
|
564
|
+
.collection_cardinality
|
|
565
|
+
.entry(capability.to_string())
|
|
566
|
+
.or_default();
|
|
567
|
+
*count = count.saturating_sub(1);
|
|
546
568
|
let _index = workload_diagnostics::span(workload_diagnostics::Phase::IndexMaintenance);
|
|
547
569
|
self.equality_index
|
|
548
570
|
.remove_record(capability, key, &removed.value);
|
|
@@ -552,6 +574,7 @@ impl Inner {
|
|
|
552
574
|
/// Drop all records, as a snapshot install does. The index follows.
|
|
553
575
|
fn clear_rows(&mut self) {
|
|
554
576
|
self.rows.clear();
|
|
577
|
+
self.collection_cardinality.clear();
|
|
555
578
|
self.equality_index.clear();
|
|
556
579
|
}
|
|
557
580
|
}
|
|
@@ -773,6 +796,10 @@ const TRANSACTION_ATTRIBUTION_KEYS: usize = 32;
|
|
|
773
796
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
774
797
|
pub struct TransactionAttribution {
|
|
775
798
|
pub transaction_id: String,
|
|
799
|
+
/// Stable operation identities assigned to the rows in this transaction.
|
|
800
|
+
/// These are distinct from both transaction and revision identity.
|
|
801
|
+
#[serde(default)]
|
|
802
|
+
pub operation_ids: Vec<String>,
|
|
776
803
|
/// The actor the caller authenticated as, when the transaction named one.
|
|
777
804
|
pub subject: Option<String>,
|
|
778
805
|
pub tenant_id: Option<String>,
|
|
@@ -789,8 +816,23 @@ pub struct TransactionAttribution {
|
|
|
789
816
|
/// How many mutations the transaction made, whether or not `keys` lists
|
|
790
817
|
/// them all.
|
|
791
818
|
pub operations: usize,
|
|
819
|
+
#[serde(default)]
|
|
820
|
+
pub principal_id: Option<String>,
|
|
821
|
+
#[serde(default)]
|
|
822
|
+
pub agent_id: Option<String>,
|
|
823
|
+
#[serde(default)]
|
|
824
|
+
pub session_id: Option<String>,
|
|
825
|
+
#[serde(default)]
|
|
826
|
+
pub delegator_id: Option<String>,
|
|
827
|
+
#[serde(default)]
|
|
828
|
+
pub work_id: Option<String>,
|
|
792
829
|
/// Milliseconds since the epoch, matching `StoredRow::unix_ms`.
|
|
793
830
|
pub unix_ms: u128,
|
|
831
|
+
/// Canonical transaction metadata exactly as committed. Record values are
|
|
832
|
+
/// deliberately absent; stable work/session/agent/operation references
|
|
833
|
+
/// live here so historical provenance survives process loss.
|
|
834
|
+
#[serde(default)]
|
|
835
|
+
pub audit: Option<Value>,
|
|
794
836
|
}
|
|
795
837
|
|
|
796
838
|
impl TransactionAttribution {
|
|
@@ -813,6 +855,13 @@ impl TransactionAttribution {
|
|
|
813
855
|
};
|
|
814
856
|
TransactionAttribution {
|
|
815
857
|
transaction_id: transaction_id.to_string(),
|
|
858
|
+
operation_ids: rows
|
|
859
|
+
.iter()
|
|
860
|
+
.filter_map(|row| row.operation.as_ref())
|
|
861
|
+
.map(|operation| {
|
|
862
|
+
format!("operation:{}:{}", operation.instance_id, operation.sequence)
|
|
863
|
+
})
|
|
864
|
+
.collect(),
|
|
816
865
|
subject: text("subject"),
|
|
817
866
|
tenant_id: text("tenant"),
|
|
818
867
|
application_id: text("application"),
|
|
@@ -827,7 +876,13 @@ impl TransactionAttribution {
|
|
|
827
876
|
.map(|row| format!("{}:{}", row.capability, row.key))
|
|
828
877
|
.collect(),
|
|
829
878
|
operations: rows.len(),
|
|
879
|
+
principal_id: text("principal_id"),
|
|
880
|
+
agent_id: text("agent_id"),
|
|
881
|
+
session_id: text("session_id"),
|
|
882
|
+
delegator_id: text("delegator_id"),
|
|
883
|
+
work_id: text("work_id").or_else(|| text("workload_id")),
|
|
830
884
|
unix_ms,
|
|
885
|
+
audit: audit.cloned(),
|
|
831
886
|
}
|
|
832
887
|
}
|
|
833
888
|
|
|
@@ -838,14 +893,84 @@ impl TransactionAttribution {
|
|
|
838
893
|
}
|
|
839
894
|
}
|
|
840
895
|
|
|
896
|
+
/// Upgrade compacted attribution written before operation identities became a
|
|
897
|
+
/// first-class part of canonical history. The mapping is deterministic from
|
|
898
|
+
/// durable transaction data, so reopening never regenerates a different ID.
|
|
899
|
+
fn normalize_transaction_attribution(
|
|
900
|
+
mut attribution: TransactionAttribution,
|
|
901
|
+
) -> TransactionAttribution {
|
|
902
|
+
if attribution.operation_ids.is_empty() {
|
|
903
|
+
if attribution.transaction_id.starts_with("operation:") {
|
|
904
|
+
let operation_id = attribution.transaction_id.clone();
|
|
905
|
+
attribution.transaction_id = operation_id.replacen("operation:", "transaction:", 1);
|
|
906
|
+
attribution.operation_ids.push(operation_id);
|
|
907
|
+
} else {
|
|
908
|
+
attribution.operation_ids = (1..=attribution.operations)
|
|
909
|
+
.map(|index| {
|
|
910
|
+
format!(
|
|
911
|
+
"operation:legacy-transaction:{}:{index}",
|
|
912
|
+
attribution.transaction_id
|
|
913
|
+
)
|
|
914
|
+
})
|
|
915
|
+
.collect();
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
attribution
|
|
919
|
+
}
|
|
920
|
+
|
|
841
921
|
/// Records an attribution, discarding the oldest once the window is full.
|
|
842
922
|
fn remember_attribution(inner: &mut Inner, attribution: TransactionAttribution) {
|
|
923
|
+
inner.transaction_history.push(attribution.clone());
|
|
843
924
|
if inner.transaction_attributions.len() >= TRANSACTION_ATTRIBUTION_WINDOW {
|
|
844
925
|
inner.transaction_attributions.pop_front();
|
|
845
926
|
}
|
|
846
927
|
inner.transaction_attributions.push_back(attribution);
|
|
847
928
|
}
|
|
848
929
|
|
|
930
|
+
fn remember_operation_attribution(
|
|
931
|
+
inner: &mut Inner,
|
|
932
|
+
row: &StoredRow,
|
|
933
|
+
base_revision: u64,
|
|
934
|
+
commit_revision: u64,
|
|
935
|
+
) {
|
|
936
|
+
let Some(operation) = row.operation.as_ref() else {
|
|
937
|
+
return;
|
|
938
|
+
};
|
|
939
|
+
remember_attribution(
|
|
940
|
+
inner,
|
|
941
|
+
TransactionAttribution {
|
|
942
|
+
transaction_id: format!(
|
|
943
|
+
"transaction:{}:{}",
|
|
944
|
+
operation.instance_id, operation.sequence
|
|
945
|
+
),
|
|
946
|
+
operation_ids: vec![format!(
|
|
947
|
+
"operation:{}:{}",
|
|
948
|
+
operation.instance_id, operation.sequence
|
|
949
|
+
)],
|
|
950
|
+
subject: None,
|
|
951
|
+
tenant_id: None,
|
|
952
|
+
application_id: None,
|
|
953
|
+
application_revision: operation
|
|
954
|
+
.revision
|
|
955
|
+
.as_ref()
|
|
956
|
+
.map(|revision| revision.id.clone()),
|
|
957
|
+
base_revision,
|
|
958
|
+
commit_revision,
|
|
959
|
+
state_before: operation.sequence.saturating_sub(1),
|
|
960
|
+
state_after: operation.sequence,
|
|
961
|
+
keys: vec![format!("{}:{}", row.capability, row.key)],
|
|
962
|
+
operations: 1,
|
|
963
|
+
principal_id: None,
|
|
964
|
+
agent_id: None,
|
|
965
|
+
session_id: None,
|
|
966
|
+
delegator_id: None,
|
|
967
|
+
work_id: None,
|
|
968
|
+
unix_ms: row.unix_ms,
|
|
969
|
+
audit: None,
|
|
970
|
+
},
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
|
|
849
974
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
850
975
|
struct TransactionLogRecord {
|
|
851
976
|
record_type: String,
|
|
@@ -1551,6 +1676,15 @@ struct LogHeader {
|
|
|
1551
1676
|
/// which deserialize as 0 and are classified as unversioned.
|
|
1552
1677
|
#[serde(default)]
|
|
1553
1678
|
format_version: u32,
|
|
1679
|
+
/// Canonical commit provenance retained when operation-log compaction
|
|
1680
|
+
/// rewrites current state. This is history, not a derived event store.
|
|
1681
|
+
#[serde(default)]
|
|
1682
|
+
transaction_history: Vec<TransactionAttribution>,
|
|
1683
|
+
/// Durable binding between a caller transaction identity and its semantic
|
|
1684
|
+
/// request. Compaction must retain this or an old id could be reused for a
|
|
1685
|
+
/// different mutation after restart.
|
|
1686
|
+
#[serde(default)]
|
|
1687
|
+
transaction_payload_hashes: HashMap<String, String>,
|
|
1554
1688
|
}
|
|
1555
1689
|
|
|
1556
1690
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
|
@@ -2658,13 +2792,19 @@ impl FeltDb {
|
|
|
2658
2792
|
.get(transaction_id)
|
|
2659
2793
|
.copied()
|
|
2660
2794
|
.unwrap_or((inner.authority_revision, inner.authority_revision));
|
|
2795
|
+
let (state_before, state_after) = inner
|
|
2796
|
+
.transaction_history
|
|
2797
|
+
.iter()
|
|
2798
|
+
.find(|attribution| attribution.transaction_id == transaction_id)
|
|
2799
|
+
.map(|attribution| (attribution.state_before, attribution.state_after))
|
|
2800
|
+
.unwrap_or((inner.sequence, inner.sequence));
|
|
2661
2801
|
return Ok(AtomicCommit {
|
|
2662
2802
|
transaction_id: transaction_id.into(),
|
|
2663
2803
|
base_revision,
|
|
2664
2804
|
commit_revision,
|
|
2665
2805
|
status: TransactionStatus::Committed,
|
|
2666
|
-
state_before
|
|
2667
|
-
state_after
|
|
2806
|
+
state_before,
|
|
2807
|
+
state_after,
|
|
2668
2808
|
rows: vec![],
|
|
2669
2809
|
duplicate: true,
|
|
2670
2810
|
});
|
|
@@ -2960,26 +3100,11 @@ impl FeltDb {
|
|
|
2960
3100
|
}
|
|
2961
3101
|
if row.deleted {
|
|
2962
3102
|
inner.remove_row(&row.capability, &row.key);
|
|
2963
|
-
// Decrement cardinality on delete
|
|
2964
|
-
let count = inner
|
|
2965
|
-
.collection_cardinality
|
|
2966
|
-
.entry(row.capability.clone())
|
|
2967
|
-
.or_default();
|
|
2968
|
-
*count = count.saturating_sub(1);
|
|
2969
3103
|
} else {
|
|
2970
3104
|
// Record and index change together, inside the commit that
|
|
2971
3105
|
// is already holding the state lock: a transaction that is
|
|
2972
3106
|
// refused leaves neither, and one that commits publishes both.
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
// Increment cardinality only on insert, not update
|
|
2976
|
-
if is_insert {
|
|
2977
|
-
let count = inner
|
|
2978
|
-
.collection_cardinality
|
|
2979
|
-
.entry(row.capability.clone())
|
|
2980
|
-
.or_default();
|
|
2981
|
-
*count = count.saturating_add(1);
|
|
2982
|
-
}
|
|
3107
|
+
inner.put_row(row.clone());
|
|
2983
3108
|
}
|
|
2984
3109
|
}
|
|
2985
3110
|
let tail_began = workload_diagnostics::enabled().then(std::time::Instant::now);
|
|
@@ -3123,7 +3248,14 @@ impl FeltDb {
|
|
|
3123
3248
|
inner.change_log.add_operation(op.clone());
|
|
3124
3249
|
|
|
3125
3250
|
append_event_locked(&mut inner, &row)?;
|
|
3251
|
+
let base_revision = inner.authority_revision;
|
|
3126
3252
|
inner.authority_revision += 1;
|
|
3253
|
+
remember_operation_attribution(
|
|
3254
|
+
&mut inner,
|
|
3255
|
+
&row,
|
|
3256
|
+
base_revision,
|
|
3257
|
+
base_revision + 1,
|
|
3258
|
+
);
|
|
3127
3259
|
inner.put_row(row);
|
|
3128
3260
|
if let Some(revision) = planned {
|
|
3129
3261
|
persist_revision_locked(&mut inner, revision)?;
|
|
@@ -3202,7 +3334,14 @@ impl FeltDb {
|
|
|
3202
3334
|
inner.change_log.add_operation(op.clone());
|
|
3203
3335
|
|
|
3204
3336
|
append_event_locked(&mut inner, &row)?;
|
|
3337
|
+
let base_revision = inner.authority_revision;
|
|
3205
3338
|
inner.authority_revision += 1;
|
|
3339
|
+
remember_operation_attribution(
|
|
3340
|
+
&mut inner,
|
|
3341
|
+
&row,
|
|
3342
|
+
base_revision,
|
|
3343
|
+
base_revision + 1,
|
|
3344
|
+
);
|
|
3206
3345
|
inner.put_row(row);
|
|
3207
3346
|
if let Some(revision) = planned {
|
|
3208
3347
|
persist_revision_locked(&mut inner, revision)?;
|
|
@@ -3270,7 +3409,14 @@ impl FeltDb {
|
|
|
3270
3409
|
operation: Some(op),
|
|
3271
3410
|
};
|
|
3272
3411
|
append_event(&inner.path, &tombstone)?;
|
|
3412
|
+
let base_revision = inner.authority_revision;
|
|
3273
3413
|
inner.authority_revision += 1;
|
|
3414
|
+
remember_operation_attribution(
|
|
3415
|
+
&mut inner,
|
|
3416
|
+
&tombstone,
|
|
3417
|
+
base_revision,
|
|
3418
|
+
base_revision + 1,
|
|
3419
|
+
);
|
|
3274
3420
|
|
|
3275
3421
|
// Remove from in-memory store, index entries included.
|
|
3276
3422
|
inner.remove_row(&capability, &key);
|
|
@@ -3879,6 +4025,8 @@ impl FeltDb {
|
|
|
3879
4025
|
format_version: DURABLE_FORMAT_VERSION,
|
|
3880
4026
|
local_sequence: inner.sequence,
|
|
3881
4027
|
observed_versions: snapshot.versions.clone(),
|
|
4028
|
+
transaction_history: Vec::new(),
|
|
4029
|
+
transaction_payload_hashes: HashMap::new(),
|
|
3882
4030
|
},
|
|
3883
4031
|
)?;
|
|
3884
4032
|
for row in &snapshot.rows {
|
|
@@ -3951,25 +4099,43 @@ impl FeltDb {
|
|
|
3951
4099
|
.collect())
|
|
3952
4100
|
}
|
|
3953
4101
|
|
|
3954
|
-
/// One transaction's attribution,
|
|
4102
|
+
/// One transaction's durable attribution, regardless of the operator
|
|
4103
|
+
/// window. This lookup is canonical historical provenance.
|
|
3955
4104
|
pub fn transaction_attribution(
|
|
3956
4105
|
&self,
|
|
3957
4106
|
transaction_id: &str,
|
|
3958
4107
|
) -> Result<Option<TransactionAttribution>> {
|
|
3959
4108
|
let inner = self.state();
|
|
3960
4109
|
Ok(inner
|
|
3961
|
-
.
|
|
4110
|
+
.transaction_history
|
|
3962
4111
|
.iter()
|
|
3963
4112
|
.rev()
|
|
3964
4113
|
.find(|attribution| attribution.transaction_id == transaction_id)
|
|
3965
4114
|
.cloned())
|
|
3966
4115
|
}
|
|
3967
4116
|
|
|
3968
|
-
///
|
|
4117
|
+
/// Canonical commit history in authority order, suitable for resumable
|
|
4118
|
+
/// event projection and historical provenance reconstruction.
|
|
4119
|
+
pub fn committed_transactions_after(
|
|
4120
|
+
&self,
|
|
4121
|
+
commit_revision: u64,
|
|
4122
|
+
limit: usize,
|
|
4123
|
+
) -> Result<Vec<TransactionAttribution>> {
|
|
4124
|
+
let inner = self.state();
|
|
4125
|
+
Ok(inner
|
|
4126
|
+
.transaction_history
|
|
4127
|
+
.iter()
|
|
4128
|
+
.filter(|transaction| transaction.commit_revision > commit_revision)
|
|
4129
|
+
.take(limit)
|
|
4130
|
+
.cloned()
|
|
4131
|
+
.collect())
|
|
4132
|
+
}
|
|
4133
|
+
|
|
4134
|
+
/// Durable commits that named this record, newest first.
|
|
3969
4135
|
///
|
|
3970
|
-
/// This
|
|
3971
|
-
///
|
|
3972
|
-
///
|
|
4136
|
+
/// This deliberately reads complete canonical history rather than the
|
|
4137
|
+
/// bounded operator window. Historical provenance must not disappear when
|
|
4138
|
+
/// a cache rolls over.
|
|
3973
4139
|
pub fn transactions_touching(
|
|
3974
4140
|
&self,
|
|
3975
4141
|
capability: &str,
|
|
@@ -3978,7 +4144,7 @@ impl FeltDb {
|
|
|
3978
4144
|
) -> Result<Vec<TransactionAttribution>> {
|
|
3979
4145
|
let inner = self.state();
|
|
3980
4146
|
Ok(inner
|
|
3981
|
-
.
|
|
4147
|
+
.transaction_history
|
|
3982
4148
|
.iter()
|
|
3983
4149
|
.rev()
|
|
3984
4150
|
.filter(|attribution| attribution.touched(capability, key))
|
|
@@ -4076,6 +4242,8 @@ impl FeltDb {
|
|
|
4076
4242
|
format_version: DURABLE_FORMAT_VERSION,
|
|
4077
4243
|
local_sequence: inner.sequence,
|
|
4078
4244
|
observed_versions: inner.change_log.versions(),
|
|
4245
|
+
transaction_history: inner.transaction_history.clone(),
|
|
4246
|
+
transaction_payload_hashes: inner.transaction_payload_hashes.clone(),
|
|
4079
4247
|
},
|
|
4080
4248
|
)?;
|
|
4081
4249
|
for operation in inner.change_log.pending_operations() {
|
|
@@ -4846,6 +5014,25 @@ fn replay_log(inner: &mut Inner) -> Result<()> {
|
|
|
4846
5014
|
.map_err(|error| corruption(line, &format!("malformed log header: {error}")))?;
|
|
4847
5015
|
inner.sequence = inner.sequence.max(header.local_sequence);
|
|
4848
5016
|
inner.change_log.observed_versions = header.observed_versions;
|
|
5017
|
+
for attribution in header.transaction_history {
|
|
5018
|
+
let attribution = normalize_transaction_attribution(attribution);
|
|
5019
|
+
inner.authority_revision = inner
|
|
5020
|
+
.authority_revision
|
|
5021
|
+
.max(attribution.commit_revision);
|
|
5022
|
+
inner
|
|
5023
|
+
.transaction_revisions
|
|
5024
|
+
.insert(
|
|
5025
|
+
attribution.transaction_id.clone(),
|
|
5026
|
+
(attribution.base_revision, attribution.commit_revision),
|
|
5027
|
+
);
|
|
5028
|
+
inner
|
|
5029
|
+
.applied_transactions
|
|
5030
|
+
.insert(attribution.transaction_id.clone());
|
|
5031
|
+
remember_attribution(inner, attribution);
|
|
5032
|
+
}
|
|
5033
|
+
inner
|
|
5034
|
+
.transaction_payload_hashes
|
|
5035
|
+
.extend(header.transaction_payload_hashes);
|
|
4849
5036
|
inner.bootstrapped = true;
|
|
4850
5037
|
snapshot_log = true;
|
|
4851
5038
|
continue;
|
|
@@ -4925,6 +5112,7 @@ fn replay_log(inner: &mut Inner) -> Result<()> {
|
|
|
4925
5112
|
}
|
|
4926
5113
|
let row: StoredRow = serde_json::from_value(value)
|
|
4927
5114
|
.map_err(|_| corruption(line, "not a durable record this build understands"))?;
|
|
5115
|
+
let derived_revision = row.capability == "state" && row.operation.is_none();
|
|
4928
5116
|
if let Some(operation) = row.operation.clone() {
|
|
4929
5117
|
// A revision is a deterministic function of the operation that
|
|
4930
5118
|
// produced it, so it never has to be lost. If the crash landed
|
|
@@ -4942,12 +5130,23 @@ fn replay_log(inner: &mut Inner) -> Result<()> {
|
|
|
4942
5130
|
inner.sync_state.merge_vector_clock(clock);
|
|
4943
5131
|
}
|
|
4944
5132
|
inner.change_log.add_operation(operation);
|
|
4945
|
-
} else if !snapshot_log {
|
|
5133
|
+
} else if !snapshot_log && !derived_revision {
|
|
4946
5134
|
// Pre-operation-envelope logs were produced only by this local
|
|
4947
5135
|
// store. Preserve a monotonic sequence when upgrading them.
|
|
4948
5136
|
inner.sequence += 1;
|
|
4949
5137
|
}
|
|
4950
|
-
|
|
5138
|
+
if !derived_revision {
|
|
5139
|
+
let base_revision = inner.authority_revision;
|
|
5140
|
+
inner.authority_revision += 1;
|
|
5141
|
+
if !snapshot_log {
|
|
5142
|
+
remember_operation_attribution(
|
|
5143
|
+
inner,
|
|
5144
|
+
&row,
|
|
5145
|
+
base_revision,
|
|
5146
|
+
base_revision + 1,
|
|
5147
|
+
);
|
|
5148
|
+
}
|
|
5149
|
+
}
|
|
4951
5150
|
if let Some(id) = row
|
|
4952
5151
|
.key
|
|
4953
5152
|
.rsplit_once(':')
|
|
@@ -5292,13 +5491,232 @@ mod tests {
|
|
|
5292
5491
|
format!("tx_{}", commits - 3),
|
|
5293
5492
|
]
|
|
5294
5493
|
);
|
|
5295
|
-
// The oldest
|
|
5296
|
-
//
|
|
5297
|
-
assert!(db
|
|
5494
|
+
// The oldest commit rolled out of the recent window but remains
|
|
5495
|
+
// addressable through canonical history.
|
|
5496
|
+
assert!(db
|
|
5497
|
+
.transaction_attribution("tx_0")
|
|
5498
|
+
.expect("lookup")
|
|
5499
|
+
.is_some());
|
|
5298
5500
|
drop(db);
|
|
5299
5501
|
let _ = fs::remove_file(path);
|
|
5300
5502
|
}
|
|
5301
5503
|
|
|
5504
|
+
#[test]
|
|
5505
|
+
fn canonical_transaction_history_survives_window_restart_and_compaction() {
|
|
5506
|
+
let path = temp_file();
|
|
5507
|
+
let db = open(&path).expect("open");
|
|
5508
|
+
let commits = TRANSACTION_ATTRIBUTION_WINDOW + 1;
|
|
5509
|
+
for index in 0..commits {
|
|
5510
|
+
db.apply_atomic_transaction(
|
|
5511
|
+
&format!("history_tx_{index}"),
|
|
5512
|
+
None,
|
|
5513
|
+
&[],
|
|
5514
|
+
&[AtomicMutation {
|
|
5515
|
+
capability: "history:Note".into(),
|
|
5516
|
+
key: format!("note_{index}"),
|
|
5517
|
+
value: Some(serde_json::json!({"index": index})),
|
|
5518
|
+
}],
|
|
5519
|
+
Some(serde_json::json!({"subject": "agent", "work_id": "work-1"})),
|
|
5520
|
+
)
|
|
5521
|
+
.expect("commit");
|
|
5522
|
+
}
|
|
5523
|
+
let complete = db.committed_transactions_after(0, usize::MAX).unwrap();
|
|
5524
|
+
assert_eq!(complete.len(), commits);
|
|
5525
|
+
assert_eq!(complete[0].transaction_id, "history_tx_0");
|
|
5526
|
+
assert_eq!(complete[0].operation_ids.len(), 1);
|
|
5527
|
+
assert_ne!(complete[0].operation_ids[0], complete[0].transaction_id);
|
|
5528
|
+
assert_eq!(complete[0].work_id.as_deref(), Some("work-1"));
|
|
5529
|
+
assert_eq!(
|
|
5530
|
+
db.transactions_touching("history:Note", "note_0", 1)
|
|
5531
|
+
.unwrap()[0]
|
|
5532
|
+
.transaction_id,
|
|
5533
|
+
"history_tx_0"
|
|
5534
|
+
);
|
|
5535
|
+
assert_eq!(
|
|
5536
|
+
db.transaction_attribution("history_tx_0")
|
|
5537
|
+
.unwrap()
|
|
5538
|
+
.unwrap()
|
|
5539
|
+
.audit
|
|
5540
|
+
.unwrap()["work_id"],
|
|
5541
|
+
"work-1"
|
|
5542
|
+
);
|
|
5543
|
+
let versions = db.operation_versions().unwrap();
|
|
5544
|
+
db.acknowledge_peer_versions("history-peer".into(), versions)
|
|
5545
|
+
.unwrap();
|
|
5546
|
+
db.compact_operation_log(&["history-peer".into()]).unwrap();
|
|
5547
|
+
drop(db);
|
|
5548
|
+
|
|
5549
|
+
let reopened = open(&path).expect("reopen");
|
|
5550
|
+
assert_eq!(
|
|
5551
|
+
reopened
|
|
5552
|
+
.committed_transactions_after(0, usize::MAX)
|
|
5553
|
+
.unwrap()
|
|
5554
|
+
.len(),
|
|
5555
|
+
commits
|
|
5556
|
+
);
|
|
5557
|
+
assert!(reopened
|
|
5558
|
+
.transaction_attribution("history_tx_0")
|
|
5559
|
+
.unwrap()
|
|
5560
|
+
.is_some());
|
|
5561
|
+
assert_eq!(
|
|
5562
|
+
reopened
|
|
5563
|
+
.transactions_touching("history:Note", "note_0", 1)
|
|
5564
|
+
.unwrap()[0]
|
|
5565
|
+
.transaction_id,
|
|
5566
|
+
"history_tx_0"
|
|
5567
|
+
);
|
|
5568
|
+
assert_eq!(
|
|
5569
|
+
reopened
|
|
5570
|
+
.transaction_attribution("history_tx_0")
|
|
5571
|
+
.unwrap()
|
|
5572
|
+
.unwrap()
|
|
5573
|
+
.operation_ids,
|
|
5574
|
+
complete[0].operation_ids
|
|
5575
|
+
);
|
|
5576
|
+
let _ = fs::remove_file(sync_metadata_path(&path));
|
|
5577
|
+
let _ = fs::remove_file(path);
|
|
5578
|
+
}
|
|
5579
|
+
|
|
5580
|
+
#[test]
|
|
5581
|
+
fn transaction_payload_binding_survives_restart_and_compaction() {
|
|
5582
|
+
let path = temp_file();
|
|
5583
|
+
let db = open(&path).expect("open");
|
|
5584
|
+
let mutation = AtomicMutation {
|
|
5585
|
+
capability: "managed:State".into(),
|
|
5586
|
+
key: "record-1".into(),
|
|
5587
|
+
value: Some(serde_json::json!({"value": 1})),
|
|
5588
|
+
};
|
|
5589
|
+
let first = db
|
|
5590
|
+
.apply_atomic_transaction_content_addressed(
|
|
5591
|
+
"managed-idempotency-1",
|
|
5592
|
+
Some("sha256:request-a"),
|
|
5593
|
+
None,
|
|
5594
|
+
&[],
|
|
5595
|
+
&[mutation.clone()],
|
|
5596
|
+
None,
|
|
5597
|
+
)
|
|
5598
|
+
.expect("initial commit");
|
|
5599
|
+
let duplicate = db
|
|
5600
|
+
.apply_atomic_transaction_content_addressed(
|
|
5601
|
+
"managed-idempotency-1",
|
|
5602
|
+
Some("sha256:request-a"),
|
|
5603
|
+
None,
|
|
5604
|
+
&[],
|
|
5605
|
+
&[mutation.clone()],
|
|
5606
|
+
None,
|
|
5607
|
+
)
|
|
5608
|
+
.expect("same semantic request");
|
|
5609
|
+
assert!(duplicate.duplicate);
|
|
5610
|
+
assert_eq!(duplicate.base_revision, first.base_revision);
|
|
5611
|
+
assert_eq!(duplicate.commit_revision, first.commit_revision);
|
|
5612
|
+
assert_eq!(duplicate.state_before, first.state_before);
|
|
5613
|
+
assert_eq!(duplicate.state_after, first.state_after);
|
|
5614
|
+
|
|
5615
|
+
let versions = db.operation_versions().unwrap();
|
|
5616
|
+
db.acknowledge_peer_versions("managed-peer".into(), versions)
|
|
5617
|
+
.unwrap();
|
|
5618
|
+
db.compact_operation_log(&["managed-peer".into()]).unwrap();
|
|
5619
|
+
drop(db);
|
|
5620
|
+
|
|
5621
|
+
let reopened = open(&path).expect("restart");
|
|
5622
|
+
let replay = reopened
|
|
5623
|
+
.apply_atomic_transaction_content_addressed(
|
|
5624
|
+
"managed-idempotency-1",
|
|
5625
|
+
Some("sha256:request-a"),
|
|
5626
|
+
None,
|
|
5627
|
+
&[],
|
|
5628
|
+
&[mutation.clone()],
|
|
5629
|
+
None,
|
|
5630
|
+
)
|
|
5631
|
+
.expect("retry after restart");
|
|
5632
|
+
assert!(replay.duplicate);
|
|
5633
|
+
assert_eq!(replay.commit_revision, first.commit_revision);
|
|
5634
|
+
let conflict = reopened
|
|
5635
|
+
.apply_atomic_transaction_content_addressed(
|
|
5636
|
+
"managed-idempotency-1",
|
|
5637
|
+
Some("sha256:request-b"),
|
|
5638
|
+
None,
|
|
5639
|
+
&[],
|
|
5640
|
+
&[mutation],
|
|
5641
|
+
None,
|
|
5642
|
+
)
|
|
5643
|
+
.unwrap_err();
|
|
5644
|
+
assert!(conflict
|
|
5645
|
+
.to_string()
|
|
5646
|
+
.contains("TRANSACTION_ID_PAYLOAD_MISMATCH:"));
|
|
5647
|
+
let _ = fs::remove_file(sync_metadata_path(&path));
|
|
5648
|
+
let _ = fs::remove_file(path);
|
|
5649
|
+
}
|
|
5650
|
+
|
|
5651
|
+
#[test]
|
|
5652
|
+
fn single_record_commits_are_canonical_history_positions() {
|
|
5653
|
+
let path = temp_file();
|
|
5654
|
+
let db = open(&path).unwrap();
|
|
5655
|
+
db.insert("orders:one", serde_json::json!({"status":"paid"}))
|
|
5656
|
+
.unwrap();
|
|
5657
|
+
let history = db.committed_transactions_after(0, 10).unwrap();
|
|
5658
|
+
assert_eq!(history.len(), 1);
|
|
5659
|
+
assert!(history[0].transaction_id.starts_with("transaction:"));
|
|
5660
|
+
assert!(history[0].operation_ids[0].starts_with("operation:"));
|
|
5661
|
+
assert_ne!(history[0].transaction_id, history[0].operation_ids[0]);
|
|
5662
|
+
assert_eq!(history[0].keys, vec!["orders:orders:one"]);
|
|
5663
|
+
drop(db);
|
|
5664
|
+
let reopened = open(&path).unwrap();
|
|
5665
|
+
assert_eq!(
|
|
5666
|
+
reopened.committed_transactions_after(0, 10).unwrap()[0].transaction_id,
|
|
5667
|
+
history[0].transaction_id
|
|
5668
|
+
);
|
|
5669
|
+
assert_eq!(
|
|
5670
|
+
reopened.committed_transactions_after(0, 10).unwrap()[0].operation_ids,
|
|
5671
|
+
history[0].operation_ids
|
|
5672
|
+
);
|
|
5673
|
+
let _ = fs::remove_file(path);
|
|
5674
|
+
}
|
|
5675
|
+
|
|
5676
|
+
#[test]
|
|
5677
|
+
fn compacted_legacy_attribution_gets_stable_distinct_operation_identity() {
|
|
5678
|
+
let legacy = TransactionAttribution {
|
|
5679
|
+
transaction_id: "legacy-transaction".into(),
|
|
5680
|
+
operation_ids: Vec::new(),
|
|
5681
|
+
subject: Some("agent-1".into()),
|
|
5682
|
+
tenant_id: Some("tenant-1".into()),
|
|
5683
|
+
application_id: Some("app-1".into()),
|
|
5684
|
+
application_revision: None,
|
|
5685
|
+
base_revision: 4,
|
|
5686
|
+
commit_revision: 5,
|
|
5687
|
+
state_before: 4,
|
|
5688
|
+
state_after: 5,
|
|
5689
|
+
keys: vec!["orders:one".into()],
|
|
5690
|
+
operations: 2,
|
|
5691
|
+
principal_id: None,
|
|
5692
|
+
agent_id: Some("agent-1".into()),
|
|
5693
|
+
session_id: None,
|
|
5694
|
+
delegator_id: None,
|
|
5695
|
+
work_id: None,
|
|
5696
|
+
unix_ms: 1,
|
|
5697
|
+
audit: None,
|
|
5698
|
+
};
|
|
5699
|
+
let first = normalize_transaction_attribution(legacy.clone());
|
|
5700
|
+
let second = normalize_transaction_attribution(legacy);
|
|
5701
|
+
assert_eq!(first, second);
|
|
5702
|
+
assert_eq!(first.transaction_id, "legacy-transaction");
|
|
5703
|
+
assert_eq!(first.operation_ids.len(), 2);
|
|
5704
|
+
assert!(first
|
|
5705
|
+
.operation_ids
|
|
5706
|
+
.iter()
|
|
5707
|
+
.all(|operation| operation.starts_with("operation:legacy-transaction:")));
|
|
5708
|
+
|
|
5709
|
+
let single = TransactionAttribution {
|
|
5710
|
+
transaction_id: "operation:authority:9".into(),
|
|
5711
|
+
operation_ids: Vec::new(),
|
|
5712
|
+
operations: 1,
|
|
5713
|
+
..first
|
|
5714
|
+
};
|
|
5715
|
+
let upgraded = normalize_transaction_attribution(single);
|
|
5716
|
+
assert_eq!(upgraded.transaction_id, "transaction:authority:9");
|
|
5717
|
+
assert_eq!(upgraded.operation_ids, vec!["operation:authority:9"]);
|
|
5718
|
+
}
|
|
5719
|
+
|
|
5302
5720
|
/// A bulk transaction names a bounded sample of keys and an exact count.
|
|
5303
5721
|
#[test]
|
|
5304
5722
|
fn a_bulk_transaction_truncates_its_key_list_and_keeps_the_count() {
|