@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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
//! Evidence for generalized version-control semantics over data representations.
|
|
2
|
+
//!
|
|
3
|
+
//! This test suite adds no new capability. It checks that the same revision
|
|
4
|
+
//! machinery behaves the same for source-shaped, artifact-shaped, and
|
|
5
|
+
//! application-shaped state.
|
|
6
|
+
|
|
7
|
+
use feltdb::{
|
|
8
|
+
reconcile, ConflictClass, ConflictClassification, ReconciliationPlan, Relationship, SemanticDiff,
|
|
9
|
+
StateStore, StateTopology,
|
|
10
|
+
};
|
|
11
|
+
use serde_json::{json, Value};
|
|
12
|
+
|
|
13
|
+
struct Scenario {
|
|
14
|
+
resource: &'static str,
|
|
15
|
+
base: Value,
|
|
16
|
+
left: Value,
|
|
17
|
+
right: Value,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[derive(Debug, PartialEq, Eq)]
|
|
21
|
+
struct Outcome {
|
|
22
|
+
relationship: Relationship,
|
|
23
|
+
has_lca: bool,
|
|
24
|
+
left_changes: usize,
|
|
25
|
+
right_changes: usize,
|
|
26
|
+
overall: ConflictClass,
|
|
27
|
+
path_conflicts: usize,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fn run_scenario(scenario: Scenario) -> Outcome {
|
|
31
|
+
let store = StateStore::new_volatile();
|
|
32
|
+
let base = store
|
|
33
|
+
.create(scenario.resource, scenario.base.to_string(), "audit".to_string())
|
|
34
|
+
.expect("base");
|
|
35
|
+
let left = store
|
|
36
|
+
.commit(scenario.left.to_string(), &base, "audit".to_string())
|
|
37
|
+
.expect("left");
|
|
38
|
+
let right = store
|
|
39
|
+
.commit(scenario.right.to_string(), &base, "audit".to_string())
|
|
40
|
+
.expect("right");
|
|
41
|
+
|
|
42
|
+
let mut topology = StateTopology::new();
|
|
43
|
+
for revision in [&base, &left, &right] {
|
|
44
|
+
topology.add_revision(revision.clone());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let left_diff = SemanticDiff::compute(&scenario.base, &scenario.left);
|
|
48
|
+
let right_diff = SemanticDiff::compute(&scenario.base, &scenario.right);
|
|
49
|
+
let classification = ConflictClassification::classify(&base, &left, &right);
|
|
50
|
+
|
|
51
|
+
let plan = ReconciliationPlan::new(left.id.clone(), right.id.clone(), base.id.clone(), true);
|
|
52
|
+
let merged = reconcile(&base, &left, &right, &plan, "reconciler".to_string()).expect("merge");
|
|
53
|
+
assert_eq!(
|
|
54
|
+
merged.materialized_state.parent_id.as_ref(),
|
|
55
|
+
Some(&left.id),
|
|
56
|
+
"the selected parent is honored independent of representation"
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
Outcome {
|
|
60
|
+
relationship: topology.relationship(&left.id, &right.id),
|
|
61
|
+
has_lca: topology.common_ancestor(&left.id, &right.id).is_some(),
|
|
62
|
+
left_changes: left_diff.changes.len(),
|
|
63
|
+
right_changes: right_diff.changes.len(),
|
|
64
|
+
overall: classification.overall,
|
|
65
|
+
path_conflicts: classification.path_conflicts.len(),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[test]
|
|
70
|
+
fn source_artifact_and_application_states_share_the_same_version_control_behavior() {
|
|
71
|
+
let source = run_scenario(Scenario {
|
|
72
|
+
resource: "source:src/main.rs",
|
|
73
|
+
base: json!({"body":"fn main() {}","digest":"h1"}),
|
|
74
|
+
left: json!({"body":"fn main() { println!(\"ok\"); }","digest":"h1"}),
|
|
75
|
+
right: json!({"body":"fn main() {}","digest":"h2"}),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
let artifact = run_scenario(Scenario {
|
|
79
|
+
resource: "artifact:dist/app.wasm",
|
|
80
|
+
base: json!({"payload":"AA==","digest":"h1"}),
|
|
81
|
+
left: json!({"payload":"AQ==","digest":"h1"}),
|
|
82
|
+
right: json!({"payload":"AA==","digest":"h2"}),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
let application = run_scenario(Scenario {
|
|
86
|
+
resource: "application:tasks/1",
|
|
87
|
+
base: json!({"state":"open","digest":"h1"}),
|
|
88
|
+
left: json!({"state":"closed","digest":"h1"}),
|
|
89
|
+
right: json!({"state":"open","digest":"h2"}),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
let expected = Outcome {
|
|
93
|
+
relationship: Relationship::Diverged,
|
|
94
|
+
has_lca: true,
|
|
95
|
+
left_changes: 1,
|
|
96
|
+
right_changes: 1,
|
|
97
|
+
overall: ConflictClass::Independent,
|
|
98
|
+
path_conflicts: 2,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
assert_eq!(source, expected);
|
|
102
|
+
assert_eq!(artifact, expected);
|
|
103
|
+
assert_eq!(application, expected);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#[test]
|
|
107
|
+
fn mutating_resource_identity_changes_ids_not_version_control_semantics() {
|
|
108
|
+
let source_namespace = run_scenario(Scenario {
|
|
109
|
+
resource: "source:module/alpha",
|
|
110
|
+
base: json!({"value":1,"tag":"stable"}),
|
|
111
|
+
left: json!({"value":2,"tag":"stable"}),
|
|
112
|
+
right: json!({"value":1,"tag":"candidate"}),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
let artifact_namespace = run_scenario(Scenario {
|
|
116
|
+
resource: "artifact:module/alpha",
|
|
117
|
+
base: json!({"value":1,"tag":"stable"}),
|
|
118
|
+
left: json!({"value":2,"tag":"stable"}),
|
|
119
|
+
right: json!({"value":1,"tag":"candidate"}),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
assert_eq!(source_namespace, artifact_namespace);
|
|
123
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
use feltdb::{
|
|
2
|
+
MaterializationError, MaterializationProjection, PathComponent, RefError, StateId, StateStore,
|
|
3
|
+
};
|
|
4
|
+
use feltdb::state_model::GarbageCollectionReport;
|
|
5
|
+
use serde_json::json;
|
|
6
|
+
use std::collections::BTreeMap;
|
|
7
|
+
use std::sync::Arc;
|
|
8
|
+
use tempfile::TempDir;
|
|
9
|
+
|
|
10
|
+
fn durable_store(name: &str) -> (TempDir, StateStore) {
|
|
11
|
+
let directory = TempDir::new().unwrap();
|
|
12
|
+
let db = Arc::new(feltdb::FeltDb::open(directory.path().join(name)).unwrap());
|
|
13
|
+
let store = StateStore::with_feltdb(db).unwrap();
|
|
14
|
+
(directory, store)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fn projection(paths: Vec<Vec<PathComponent>>) -> MaterializationProjection {
|
|
18
|
+
MaterializationProjection::select(paths)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#[test]
|
|
22
|
+
fn refs_cas_enforcement_rejects_stale_moves() {
|
|
23
|
+
let store = StateStore::new_volatile();
|
|
24
|
+
let a = store
|
|
25
|
+
.create("docs:1", json!({"n": 1}).to_string(), "auth".into())
|
|
26
|
+
.unwrap();
|
|
27
|
+
let b = store
|
|
28
|
+
.commit(json!({"n": 2}).to_string(), &a, "auth".into())
|
|
29
|
+
.unwrap();
|
|
30
|
+
store
|
|
31
|
+
.create_ref("refs", "main", a.id.clone(), "auth".into(), BTreeMap::new())
|
|
32
|
+
.unwrap();
|
|
33
|
+
|
|
34
|
+
store
|
|
35
|
+
.compare_and_swap_ref("refs", "main", 1, b.id.clone(), "auth".into(), None)
|
|
36
|
+
.unwrap();
|
|
37
|
+
let stale = store.compare_and_swap_ref("refs", "main", 1, a.id.clone(), "auth".into(), None);
|
|
38
|
+
assert!(matches!(
|
|
39
|
+
stale,
|
|
40
|
+
Err(RefError::VersionConflict {
|
|
41
|
+
expected: 1,
|
|
42
|
+
current: 2
|
|
43
|
+
})
|
|
44
|
+
));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[test]
|
|
48
|
+
fn refs_persist_target_and_versions_across_restart() {
|
|
49
|
+
let (directory, store) = durable_store("refs.log");
|
|
50
|
+
let a = store
|
|
51
|
+
.create("docs:1", json!({"n": 1}).to_string(), "auth".into())
|
|
52
|
+
.unwrap();
|
|
53
|
+
let b = store
|
|
54
|
+
.commit(json!({"n": 2}).to_string(), &a, "auth".into())
|
|
55
|
+
.unwrap();
|
|
56
|
+
|
|
57
|
+
store
|
|
58
|
+
.create_ref("refs", "main", a.id.clone(), "auth".into(), BTreeMap::new())
|
|
59
|
+
.unwrap();
|
|
60
|
+
let moved = store.move_ref("refs", "main", b.id.clone(), "auth2".into()).unwrap();
|
|
61
|
+
assert_eq!(moved.target, b.id);
|
|
62
|
+
assert_eq!(moved.version, 2);
|
|
63
|
+
|
|
64
|
+
let reopened = StateStore::with_feltdb(Arc::new(
|
|
65
|
+
feltdb::FeltDb::open(directory.path().join("refs.log")).unwrap(),
|
|
66
|
+
))
|
|
67
|
+
.unwrap();
|
|
68
|
+
let persisted = reopened.get_ref("refs", "main").unwrap();
|
|
69
|
+
assert_eq!(persisted.target, moved.target);
|
|
70
|
+
assert_eq!(persisted.version, moved.version);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#[test]
|
|
74
|
+
fn refs_namespace_isolation_deletion_and_enumeration() {
|
|
75
|
+
let store = StateStore::new_volatile();
|
|
76
|
+
let revision = store
|
|
77
|
+
.create("docs:1", json!({"n": 1}).to_string(), "auth".into())
|
|
78
|
+
.unwrap();
|
|
79
|
+
|
|
80
|
+
store
|
|
81
|
+
.create_ref("refs", "main", revision.id.clone(), "auth".into(), BTreeMap::new())
|
|
82
|
+
.unwrap();
|
|
83
|
+
store
|
|
84
|
+
.create_ref("tags", "main", revision.id.clone(), "auth".into(), BTreeMap::new())
|
|
85
|
+
.unwrap();
|
|
86
|
+
|
|
87
|
+
let refs_all = store.list_refs(None);
|
|
88
|
+
assert_eq!(refs_all.len(), 2);
|
|
89
|
+
assert_eq!(store.list_refs(Some("refs")).len(), 1);
|
|
90
|
+
assert_eq!(store.list_refs(Some("tags")).len(), 1);
|
|
91
|
+
|
|
92
|
+
store.delete_ref("tags", "main", Some(1)).unwrap();
|
|
93
|
+
assert!(store.get_ref("tags", "main").is_none());
|
|
94
|
+
assert!(store.get_ref("refs", "main").is_some());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#[test]
|
|
98
|
+
fn materialization_projection_filters_revision_selection_and_nested_paths() {
|
|
99
|
+
let store = StateStore::new_volatile();
|
|
100
|
+
let base = store
|
|
101
|
+
.create(
|
|
102
|
+
"source:file",
|
|
103
|
+
json!({"tree":{"src":{"lib.rs":"v1","main.rs":"m1"}},"meta":{"lang":"rust"}})
|
|
104
|
+
.to_string(),
|
|
105
|
+
"auth".into(),
|
|
106
|
+
)
|
|
107
|
+
.unwrap();
|
|
108
|
+
let head = store
|
|
109
|
+
.commit(
|
|
110
|
+
json!({"tree":{"src":{"lib.rs":"v2","main.rs":"m1"}},"meta":{"lang":"rust"}})
|
|
111
|
+
.to_string(),
|
|
112
|
+
&base,
|
|
113
|
+
"auth".into(),
|
|
114
|
+
)
|
|
115
|
+
.unwrap();
|
|
116
|
+
|
|
117
|
+
let view = store
|
|
118
|
+
.materialize(
|
|
119
|
+
&head.id,
|
|
120
|
+
&projection(vec![
|
|
121
|
+
vec![PathComponent::Key("tree".into()), PathComponent::Key("src".into()), PathComponent::Key("lib.rs".into())],
|
|
122
|
+
]),
|
|
123
|
+
)
|
|
124
|
+
.unwrap();
|
|
125
|
+
assert_eq!(view, json!({"tree":{"src":{"lib.rs":"v2"}}}));
|
|
126
|
+
|
|
127
|
+
let base_view = store
|
|
128
|
+
.materialize(
|
|
129
|
+
&base.id,
|
|
130
|
+
&projection(vec![
|
|
131
|
+
vec![PathComponent::Key("tree".into()), PathComponent::Key("src".into()), PathComponent::Key("lib.rs".into())],
|
|
132
|
+
]),
|
|
133
|
+
)
|
|
134
|
+
.unwrap();
|
|
135
|
+
assert_eq!(base_view, json!({"tree":{"src":{"lib.rs":"v1"}}}));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#[test]
|
|
139
|
+
fn materialization_missing_paths_and_deterministic_output_are_enforced() {
|
|
140
|
+
let store = StateStore::new_volatile();
|
|
141
|
+
let revision = store
|
|
142
|
+
.create(
|
|
143
|
+
"artifact:item",
|
|
144
|
+
json!({"manifest":{"digest":"abc","size":7},"chunks":[{"id":1},{"id":2}]}).to_string(),
|
|
145
|
+
"auth".into(),
|
|
146
|
+
)
|
|
147
|
+
.unwrap();
|
|
148
|
+
|
|
149
|
+
let missing = store.materialize(
|
|
150
|
+
&revision.id,
|
|
151
|
+
&projection(vec![vec![
|
|
152
|
+
PathComponent::Key("manifest".into()),
|
|
153
|
+
PathComponent::Key("missing".into()),
|
|
154
|
+
]]),
|
|
155
|
+
);
|
|
156
|
+
assert!(matches!(missing, Err(MaterializationError::MissingPath(_))));
|
|
157
|
+
|
|
158
|
+
let first = store
|
|
159
|
+
.materialize(
|
|
160
|
+
&revision.id,
|
|
161
|
+
&projection(vec![
|
|
162
|
+
vec![PathComponent::Key("manifest".into()), PathComponent::Key("size".into())],
|
|
163
|
+
vec![PathComponent::Key("chunks".into()), PathComponent::Index(1)],
|
|
164
|
+
]),
|
|
165
|
+
)
|
|
166
|
+
.unwrap();
|
|
167
|
+
let second = store
|
|
168
|
+
.materialize(
|
|
169
|
+
&revision.id,
|
|
170
|
+
&projection(vec![
|
|
171
|
+
vec![PathComponent::Key("chunks".into()), PathComponent::Index(1)],
|
|
172
|
+
vec![PathComponent::Key("manifest".into()), PathComponent::Key("size".into())],
|
|
173
|
+
]),
|
|
174
|
+
)
|
|
175
|
+
.unwrap();
|
|
176
|
+
assert_eq!(first, second);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fn setup_gc_graph(store: &StateStore) -> (StateId, StateId, StateId, StateId) {
|
|
180
|
+
let a = store
|
|
181
|
+
.create("graph:r", json!({"node":"A"}).to_string(), "auth".into())
|
|
182
|
+
.unwrap();
|
|
183
|
+
let b = store
|
|
184
|
+
.commit(json!({"node":"B"}).to_string(), &a, "auth".into())
|
|
185
|
+
.unwrap();
|
|
186
|
+
let c = store
|
|
187
|
+
.commit(json!({"node":"C"}).to_string(), &b, "auth".into())
|
|
188
|
+
.unwrap();
|
|
189
|
+
let d = store
|
|
190
|
+
.commit(json!({"node":"D"}).to_string(), &b, "auth".into())
|
|
191
|
+
.unwrap();
|
|
192
|
+
(a.id, b.id, c.id, d.id)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fn collect(store: &StateStore) -> GarbageCollectionReport {
|
|
196
|
+
store.collect_unreachable().unwrap()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#[test]
|
|
200
|
+
fn gc_root_discovery_and_unreachable_collection() {
|
|
201
|
+
let store = StateStore::new_volatile();
|
|
202
|
+
let (_a, _b, _c, _d) = setup_gc_graph(&store);
|
|
203
|
+
let report = collect(&store);
|
|
204
|
+
assert_eq!(report.roots.len(), 0);
|
|
205
|
+
assert_eq!(report.reachable_revisions.len(), 0);
|
|
206
|
+
assert_eq!(report.reachable_objects.len(), 0);
|
|
207
|
+
assert_eq!(report.collected_revisions.len(), 4);
|
|
208
|
+
assert_eq!(report.collected_objects.len(), 4);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
#[test]
|
|
212
|
+
fn gc_parent_and_content_traversal_preserves_shared_history() {
|
|
213
|
+
let store = StateStore::new_volatile();
|
|
214
|
+
let (a, b, c, d) = setup_gc_graph(&store);
|
|
215
|
+
store
|
|
216
|
+
.create_ref("refs", "c", c.clone(), "auth".into(), BTreeMap::new())
|
|
217
|
+
.unwrap();
|
|
218
|
+
store
|
|
219
|
+
.create_ref("refs", "d", d.clone(), "auth".into(), BTreeMap::new())
|
|
220
|
+
.unwrap();
|
|
221
|
+
|
|
222
|
+
let keep_all = collect(&store);
|
|
223
|
+
assert!(keep_all.collected_revisions.is_empty());
|
|
224
|
+
|
|
225
|
+
store.delete_ref("refs", "d", Some(1)).unwrap();
|
|
226
|
+
let after_delete = collect(&store);
|
|
227
|
+
assert!(after_delete.collected_revisions.contains(&d));
|
|
228
|
+
assert!(!after_delete.collected_revisions.contains(&b));
|
|
229
|
+
assert!(!after_delete.collected_revisions.contains(&c));
|
|
230
|
+
assert!(!after_delete.collected_revisions.contains(&a));
|
|
231
|
+
assert_eq!(after_delete.collected_revisions.len(), 1);
|
|
232
|
+
assert_eq!(after_delete.collected_objects.len(), 1);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
#[test]
|
|
236
|
+
fn substrate_is_domain_neutral_for_refs_materialization_and_gc() {
|
|
237
|
+
let store = StateStore::new_volatile();
|
|
238
|
+
|
|
239
|
+
let scenarios = vec![
|
|
240
|
+
(
|
|
241
|
+
"source:module",
|
|
242
|
+
json!({"tree":{"src":{"lib.rs":"v1","main.rs":"v1"}}}),
|
|
243
|
+
vec![PathComponent::Key("tree".into()), PathComponent::Key("src".into()), PathComponent::Key("lib.rs".into())],
|
|
244
|
+
),
|
|
245
|
+
(
|
|
246
|
+
"artifact:bundle",
|
|
247
|
+
json!({"manifest":{"digest":"h1"},"payload":{"chunks":["a","b"]}}),
|
|
248
|
+
vec![PathComponent::Key("manifest".into()), PathComponent::Key("digest".into())],
|
|
249
|
+
),
|
|
250
|
+
(
|
|
251
|
+
"application:state",
|
|
252
|
+
json!({"entities":{"tasks":{"1":{"status":"open"}}},"meta":{"v":1}}),
|
|
253
|
+
vec![PathComponent::Key("entities".into()), PathComponent::Key("tasks".into()), PathComponent::Key("1".into()), PathComponent::Key("status".into())],
|
|
254
|
+
),
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
for (resource, content, path) in scenarios {
|
|
258
|
+
let rev = store
|
|
259
|
+
.create(resource, content.to_string(), "auth".into())
|
|
260
|
+
.unwrap();
|
|
261
|
+
store
|
|
262
|
+
.create_ref("heads", resource, rev.id.clone(), "auth".into(), BTreeMap::new())
|
|
263
|
+
.unwrap();
|
|
264
|
+
let view = store
|
|
265
|
+
.materialize(&rev.id, &projection(vec![path]))
|
|
266
|
+
.unwrap();
|
|
267
|
+
assert!(view.is_object());
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let keep = collect(&store);
|
|
271
|
+
assert_eq!(keep.collected_revisions.len(), 0);
|
|
272
|
+
|
|
273
|
+
for reference in store.list_refs(Some("heads")) {
|
|
274
|
+
store
|
|
275
|
+
.delete_ref("heads", &reference.name, Some(reference.version))
|
|
276
|
+
.unwrap();
|
|
277
|
+
}
|
|
278
|
+
let collected = collect(&store);
|
|
279
|
+
assert_eq!(collected.collected_revisions.len(), 3);
|
|
280
|
+
assert_eq!(collected.collected_objects.len(), 3);
|
|
281
|
+
}
|
|
@@ -1018,9 +1018,21 @@ fn retention_cost_grows_with_the_window_and_inflates_the_log() {
|
|
|
1018
1018
|
(start.elapsed(), std::fs::metadata(&path).unwrap().len())
|
|
1019
1019
|
}
|
|
1020
1020
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1021
|
+
// Compare the best of repeated independent runs. The workspace executes
|
|
1022
|
+
// other crash/soak processes concurrently, so a single wall-clock sample
|
|
1023
|
+
// can measure scheduler delay rather than retention work. Taking the
|
|
1024
|
+
// minimum preserves the envelope claim while filtering that one-sided
|
|
1025
|
+
// noise; every sample still performs the complete durable workload.
|
|
1026
|
+
fn sample(run_once: impl Fn() -> (std::time::Duration, u64)) -> (std::time::Duration, u64) {
|
|
1027
|
+
(0..3)
|
|
1028
|
+
.map(|_| run_once())
|
|
1029
|
+
.min_by_key(|(elapsed, _)| *elapsed)
|
|
1030
|
+
.unwrap()
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
let (none_time, none_bytes) = sample(|| run(None));
|
|
1034
|
+
let (small_time, small_bytes) = sample(|| run(Some(5)));
|
|
1035
|
+
let (large_time, _) = sample(|| run(Some(50)));
|
|
1024
1036
|
|
|
1025
1037
|
assert!(
|
|
1026
1038
|
large_time > small_time,
|
package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs
CHANGED
|
@@ -74,7 +74,17 @@ fn compacted_log_after_revisions(count: usize, pad: usize) -> String {
|
|
|
74
74
|
|
|
75
75
|
fn revision_lines(log: &str) -> usize {
|
|
76
76
|
log.lines()
|
|
77
|
-
.
|
|
77
|
+
.filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
|
|
78
|
+
.filter(|record| {
|
|
79
|
+
record
|
|
80
|
+
.get("capability")
|
|
81
|
+
.and_then(serde_json::Value::as_str)
|
|
82
|
+
== Some("state")
|
|
83
|
+
&& record
|
|
84
|
+
.get("key")
|
|
85
|
+
.and_then(serde_json::Value::as_str)
|
|
86
|
+
.is_some_and(|key| key.starts_with("state:revision:"))
|
|
87
|
+
})
|
|
78
88
|
.count()
|
|
79
89
|
}
|
|
80
90
|
|
|
@@ -188,13 +198,12 @@ fn each_compaction_rewrites_the_whole_revision_history() {
|
|
|
188
198
|
// Cost: what a revision is worth in bytes
|
|
189
199
|
// ---------------------------------------------------------------------------
|
|
190
200
|
|
|
191
|
-
/// **A revision
|
|
192
|
-
/// copy of its own state.**
|
|
201
|
+
/// **A revision and its canonical transaction attribution have linear cost.**
|
|
193
202
|
///
|
|
194
203
|
/// The compacted log is linear in both the revision count and the state size:
|
|
195
204
|
///
|
|
196
205
|
/// ```text
|
|
197
|
-
/// bytes = header + count × (fixed_overhead + |content|)
|
|
206
|
+
/// bytes = header + count × (fixed_overhead + 2 × |content|)
|
|
198
207
|
/// ```
|
|
199
208
|
///
|
|
200
209
|
/// The fixed part is not incidental — it is a 64-character hex identity, a
|
|
@@ -203,9 +212,10 @@ fn each_compaction_rewrites_the_whole_revision_history() {
|
|
|
203
212
|
/// key and the fully-qualified Rust type name. It dominates until the state
|
|
204
213
|
/// being versioned is itself several hundred bytes.
|
|
205
214
|
///
|
|
206
|
-
/// The state is stored
|
|
207
|
-
///
|
|
208
|
-
///
|
|
215
|
+
/// The state is stored once in the revision and once in the canonical
|
|
216
|
+
/// transaction attribution retained for replay and provenance. The latter is
|
|
217
|
+
/// history, rather than a duplicate authority, and survives compaction by
|
|
218
|
+
/// design.
|
|
209
219
|
#[test]
|
|
210
220
|
fn a_revision_costs_a_fixed_overhead_plus_one_copy_of_its_state() {
|
|
211
221
|
let small_ten = compacted_log_after_revisions(10, 0).len();
|
|
@@ -218,27 +228,27 @@ fn a_revision_costs_a_fixed_overhead_plus_one_copy_of_its_state() {
|
|
|
218
228
|
let ten_more = small_twenty - small_ten - 10;
|
|
219
229
|
let per_revision = ten_more / 10;
|
|
220
230
|
// The shape is unchanged by the resource-scoped model; the constant is
|
|
221
|
-
// not. A revision now
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
// measurement.
|
|
231
|
+
// not. A revision now carries its resource, identity and sequence, while
|
|
232
|
+
// the snapshot header also retains the canonical transaction attribution
|
|
233
|
+
// required for replay and provenance. This assertion catches a change in
|
|
234
|
+
// the combined linear shape rather than pretending that history is free.
|
|
226
235
|
assert!(
|
|
227
|
-
(
|
|
236
|
+
(1_800..=2_300).contains(&per_revision),
|
|
228
237
|
"a revision of a 16-byte state costs {per_revision} bytes; \
|
|
229
238
|
the overhead is the record, not the state"
|
|
230
239
|
);
|
|
231
240
|
|
|
232
|
-
// Content is
|
|
233
|
-
//
|
|
241
|
+
// Content is represented in both the revision and its canonical
|
|
242
|
+
// transaction attribution: growing each state by N bytes grows the log by
|
|
243
|
+
// exactly count × 2N.
|
|
234
244
|
assert_eq!(
|
|
235
245
|
padded_hundred - small_ten,
|
|
236
|
-
10 * 100,
|
|
237
|
-
"
|
|
246
|
+
10 * 2 * 100,
|
|
247
|
+
"state appears in the revision and canonical transaction attribution"
|
|
238
248
|
);
|
|
239
249
|
assert_eq!(
|
|
240
250
|
padded_thousand - small_ten,
|
|
241
|
-
10 * 1000,
|
|
251
|
+
10 * 2 * 1000,
|
|
242
252
|
"the relationship stays exactly linear at a kilobyte of state"
|
|
243
253
|
);
|
|
244
254
|
}
|