@feltdb/core 0.8.3 → 0.8.5
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 +4 -1
- package/dist/cli/provisioning-neutrality.js +79 -0
- package/dist/collection.d.ts +43 -1
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +192 -22
- package/dist/create/create.js +25 -21
- package/dist/create/managed-account.js +11 -0
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +165 -0
- package/dist/create/server-source/Cargo.toml +9 -0
- package/dist/create/server-source/crates/feltdb/Cargo.toml +3 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +44 -44
- package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +12 -24
- package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +7 -13
- package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +13 -10
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +12 -9
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +78 -25
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +16 -13
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +13 -5
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +41 -22
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +33 -15
- package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +100 -33
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +56 -20
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +116 -41
- package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +186 -55
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +64 -26
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +46 -15
- package/dist/create/server-source/crates/feltdb/src/admission.rs +8 -15
- package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +43 -13
- package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +15 -42
- package/dist/create/server-source/crates/feltdb/src/analytics.rs +65 -19
- package/dist/create/server-source/crates/feltdb/src/application.rs +113 -30
- package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +475 -140
- package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +17 -15
- package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -1
- package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +59 -15
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +266 -114
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +25 -7
- package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +10 -15
- package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +3 -11
- package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +14 -5
- package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +21 -25
- package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +14 -11
- package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +103 -22
- package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +18 -15
- package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +12 -8
- package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +1 -5
- package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +87 -23
- package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +3 -7
- package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +10 -9
- package/dist/create/server-source/crates/feltdb/src/equality_index.rs +595 -0
- package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +1 -6
- package/dist/create/server-source/crates/feltdb/src/indexing.rs +35 -38
- package/dist/create/server-source/crates/feltdb/src/lib.rs +1050 -117
- package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +4 -1
- package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -1
- package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +1 -2
- package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +107 -30
- package/dist/create/server-source/crates/feltdb/src/observability.rs +19 -6
- package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +12 -11
- package/dist/create/server-source/crates/feltdb/src/operation_log.rs +9 -4
- package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +65 -18
- package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +193 -57
- package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +37 -27
- package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +38 -10
- package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +20 -14
- package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +394 -229
- package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +8 -6
- package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +11 -13
- package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +90 -72
- package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +3 -3
- package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +33 -11
- package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +6 -6
- package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +14 -56
- package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +29 -27
- package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +35 -17
- package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +8 -2
- package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +59 -15
- package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +70 -17
- package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +23 -6
- package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +12 -3
- package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +41 -11
- package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +56 -25
- package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +701 -245
- package/dist/create/server-source/crates/feltdb/src/production_api.rs +31 -13
- package/dist/create/server-source/crates/feltdb/src/query_execution_diagnostics.rs +126 -0
- package/dist/create/server-source/crates/feltdb/src/query_performance.rs +6 -8
- package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +5 -5
- package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +48 -18
- package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +30 -11
- package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +6 -3
- package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +4 -3
- package/dist/create/server-source/crates/feltdb/src/sharding.rs +36 -10
- package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +516 -0
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +305 -6
- package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +222 -0
- package/dist/create/server-source/crates/feltdb/src/state_facade.rs +82 -54
- package/dist/create/server-source/crates/feltdb/src/state_hash.rs +2 -2
- package/dist/create/server-source/crates/feltdb/src/state_model.rs +1514 -537
- package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +6 -3
- package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +672 -0
- package/dist/create/server-source/crates/feltdb/src/submission.rs +5 -11
- package/dist/create/server-source/crates/feltdb/src/sync.rs +12 -0
- package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +6 -8
- package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +24 -35
- package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +24 -8
- package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +248 -59
- package/dist/create/server-source/crates/feltdb/src/transactions.rs +17 -20
- package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +749 -0
- package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +1 -0
- package/dist/create/server-source/crates/feltdb/src/workload.rs +512 -4
- package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +442 -0
- package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +299 -0
- package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +288 -0
- package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +392 -0
- package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +436 -220
- package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +1916 -0
- package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +1878 -0
- package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +1862 -0
- package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +234 -0
- package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +947 -0
- package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +5 -8
- package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +52 -44
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +33 -16
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +22 -7
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +41 -22
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +25 -8
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +9 -6
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +120 -69
- package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +21 -10
- package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +1032 -0
- package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +868 -0
- package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +221 -0
- package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +338 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +1039 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +739 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +427 -0
- package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +3 -3
- package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +25 -22
- package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +1799 -0
- package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +1316 -0
- package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +53 -61
- package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +156 -61
- package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +299 -0
- package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +255 -0
- package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +249 -0
- package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +994 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +2 -1
- package/dist/create/server-source/crates/feltdb-server/src/audit.rs +1137 -29
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +164 -13
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +800 -34
- package/dist/db.d.ts +33 -34
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +74 -20
- package/dist/deployment.d.ts +30 -0
- package/dist/deployment.d.ts.map +1 -0
- package/dist/deployment.js +130 -0
- package/dist/embedded-transaction.d.ts +22 -4
- package/dist/embedded-transaction.d.ts.map +1 -1
- package/dist/embedded-transaction.js +51 -5
- package/dist/feltdb.d.ts +14 -2
- package/dist/feltdb.d.ts.map +1 -1
- package/dist/file-db.js +1 -1
- package/dist/http-client.d.ts +14 -0
- package/dist/http-client.d.ts.map +1 -1
- package/dist/http-client.js +23 -5
- package/dist/http-db.d.ts +119 -1
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +346 -31
- package/dist/index-core.d.ts +2 -0
- package/dist/index-core.d.ts.map +1 -1
- package/dist/index-core.js +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/indexeddb-db.d.ts.map +1 -1
- package/dist/indexeddb-db.js +35 -21
- package/dist/managed-recovery.d.ts +192 -0
- package/dist/managed-recovery.d.ts.map +1 -0
- package/dist/managed-recovery.js +242 -0
- package/dist/memory-db.js +1 -1
- package/dist/studio-app/assets/{feltdb_wasm-DB8cX151.js → feltdb_wasm-DaNwCLRX.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-DnsHNv6g.wasm +0 -0
- package/dist/studio-app/assets/index-j8IlhNqJ.js +29 -0
- package/dist/studio-app/index.html +1 -1
- package/dist/transaction.d.ts +30 -0
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +41 -0
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workload.d.ts +2 -1
- package/dist/workload.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-ClhDHp0S.wasm +0 -0
- package/dist/studio-app/assets/index-B0k4UAlI.js +0 -29
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
//! Evidence for the reconciliation reachability audit
|
|
2
|
+
//! (`docs/audits/RECONCILIATION-REACHABILITY-AUDIT.md`).
|
|
3
|
+
//!
|
|
4
|
+
//! This file adds **no capability and no product surface**. It exists to settle
|
|
5
|
+
//! one constructive question the audit had to answer before it could report a
|
|
6
|
+
//! blockage honestly:
|
|
7
|
+
//!
|
|
8
|
+
//! > If a `ReconciliationPlan` cannot be serialized to JSON, can it cross a
|
|
9
|
+
//! > wire at all?
|
|
10
|
+
//!
|
|
11
|
+
//! It can. The recorded limitation is narrower than it first appeared, and the
|
|
12
|
+
//! difference matters, because "the plan cannot be transmitted" would have been
|
|
13
|
+
//! a reason no API could exist. That is not the reason. The reason is in the
|
|
14
|
+
//! audit, and it is about where state lives, not about encoding.
|
|
15
|
+
//!
|
|
16
|
+
//! The DTO below is written here rather than in `src/` deliberately: proving a
|
|
17
|
+
//! shape works is not the same as committing the product to it, and this PR
|
|
18
|
+
//! adds no API.
|
|
19
|
+
|
|
20
|
+
use feltdb::state_model::PathComponent;
|
|
21
|
+
use serde::{Deserialize, Serialize};
|
|
22
|
+
use serde_json::{json, Value};
|
|
23
|
+
use std::collections::BTreeMap;
|
|
24
|
+
|
|
25
|
+
fn key(name: &str) -> PathComponent {
|
|
26
|
+
PathComponent::Key(name.to_string())
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// One override as it would appear on a wire: a *structural* path and a value.
|
|
30
|
+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
31
|
+
struct WireOverride {
|
|
32
|
+
path: Vec<PathComponent>,
|
|
33
|
+
value: Value,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// A plan as it would appear on a wire.
|
|
37
|
+
///
|
|
38
|
+
/// The one difference from `ReconciliationPlan` is that `path_overrides` is a
|
|
39
|
+
/// **list**, not a map, because JSON object keys must be strings and a path is
|
|
40
|
+
/// not one. Everything else is carried unchanged.
|
|
41
|
+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
42
|
+
struct WirePlan {
|
|
43
|
+
left_id: String,
|
|
44
|
+
right_id: String,
|
|
45
|
+
base_id: String,
|
|
46
|
+
parent_choice: bool,
|
|
47
|
+
path_overrides: Vec<WireOverride>,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// The limitation is about maps, not about paths
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
/// A path is JSON-encodable, structurally, with no invented syntax.
|
|
55
|
+
///
|
|
56
|
+
/// `ReconciliationPlan` cannot be serialized because `path_overrides` is keyed
|
|
57
|
+
/// by `Vec<PathComponent>` and JSON object keys must be strings. That is a
|
|
58
|
+
/// property of using a path as a *map key*. `PathComponent` itself derives
|
|
59
|
+
/// `Serialize`/`Deserialize` and encodes as a tagged object, so a path is
|
|
60
|
+
/// perfectly transmissible as a JSON array.
|
|
61
|
+
#[test]
|
|
62
|
+
fn a_path_survives_json_as_a_structural_array() {
|
|
63
|
+
let path = vec![key("o"), PathComponent::Index(0), key("a")];
|
|
64
|
+
let text = serde_json::to_string(&path).expect("a path serializes");
|
|
65
|
+
assert_eq!(text, r#"[{"Key":"o"},{"Index":0},{"Key":"a"}]"#);
|
|
66
|
+
|
|
67
|
+
let back: Vec<PathComponent> = serde_json::from_str(&text).expect("and parses");
|
|
68
|
+
assert_eq!(back, path, "and round trips exactly");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// The two traps the conflict work rejected flattened renderings over survive.
|
|
72
|
+
///
|
|
73
|
+
/// This is the reason the encoding is an array of tagged components rather than
|
|
74
|
+
/// a string. Under `"a.b"` / `"a.bc"` one path is a prefix of the other and the
|
|
75
|
+
/// overlap check would report a containment that does not exist; under any
|
|
76
|
+
/// string rendering an object member named `0` and array index `0` collide.
|
|
77
|
+
#[test]
|
|
78
|
+
fn the_encoding_preserves_the_distinctions_a_flattened_one_would_lose() {
|
|
79
|
+
let a_b = vec![key("a"), key("b")];
|
|
80
|
+
let a_bc = vec![key("a"), key("bc")];
|
|
81
|
+
assert_ne!(
|
|
82
|
+
serde_json::to_string(&a_b).unwrap(),
|
|
83
|
+
serde_json::to_string(&a_bc).unwrap(),
|
|
84
|
+
"['a','b'] and ['a','bc'] stay distinct, so neither contains the other"
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
let member_zero = vec![key("0")];
|
|
88
|
+
let index_zero = vec![PathComponent::Index(0)];
|
|
89
|
+
assert_eq!(
|
|
90
|
+
serde_json::to_string(&member_zero).unwrap(),
|
|
91
|
+
r#"[{"Key":"0"}]"#
|
|
92
|
+
);
|
|
93
|
+
assert_eq!(
|
|
94
|
+
serde_json::to_string(&index_zero).unwrap(),
|
|
95
|
+
r#"[{"Index":0}]"#
|
|
96
|
+
);
|
|
97
|
+
assert_ne!(
|
|
98
|
+
serde_json::to_string(&member_zero).unwrap(),
|
|
99
|
+
serde_json::to_string(&index_zero).unwrap(),
|
|
100
|
+
"an object member named 0 and the first array element stay different places"
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// A whole plan crosses a wire when overrides are a list rather than a map.
|
|
105
|
+
#[test]
|
|
106
|
+
fn a_plan_crosses_a_wire_when_its_overrides_are_a_list() {
|
|
107
|
+
let plan = WirePlan {
|
|
108
|
+
left_id: "aa".into(),
|
|
109
|
+
right_id: "bb".into(),
|
|
110
|
+
base_id: "cc".into(),
|
|
111
|
+
parent_choice: true,
|
|
112
|
+
path_overrides: vec![
|
|
113
|
+
WireOverride {
|
|
114
|
+
path: vec![key("o"), key("a")],
|
|
115
|
+
value: json!(9),
|
|
116
|
+
},
|
|
117
|
+
WireOverride {
|
|
118
|
+
path: vec![key("xs"), PathComponent::Index(1)],
|
|
119
|
+
value: json!("x"),
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
let text = serde_json::to_string(&plan).expect("a wire plan serializes");
|
|
125
|
+
let back: WirePlan = serde_json::from_str(&text).expect("and parses");
|
|
126
|
+
assert_eq!(back, plan, "and round trips exactly");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// What the list costs
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
/// The wire form reintroduces a hazard the in-memory type forecloses.
|
|
134
|
+
///
|
|
135
|
+
/// `ReconciliationPlan::path_overrides` is a `BTreeMap`, so two overrides at the
|
|
136
|
+
/// same path cannot exist — the type makes the question unaskable, and
|
|
137
|
+
/// `a_repeated_override_path_is_impossible_by_construction` records that. A
|
|
138
|
+
/// *list* has no such property: a client can send the same path twice.
|
|
139
|
+
///
|
|
140
|
+
/// The danger is not that duplicates are possible. It is that collecting the
|
|
141
|
+
/// list into the map **silently resolves them, last-wins**, which is exactly the
|
|
142
|
+
/// shape of defect this sequence has twice corrected: a plausible-looking
|
|
143
|
+
/// collapse that quietly discards one of two competing decisions. An API built
|
|
144
|
+
/// on this DTO must reject duplicates explicitly, and this test exists so that
|
|
145
|
+
/// obligation is recorded before any API exists rather than discovered after.
|
|
146
|
+
#[test]
|
|
147
|
+
fn collecting_a_wire_list_into_a_map_silently_drops_a_duplicate_path() {
|
|
148
|
+
let duplicated = vec![
|
|
149
|
+
WireOverride {
|
|
150
|
+
path: vec![key("o")],
|
|
151
|
+
value: json!("first"),
|
|
152
|
+
},
|
|
153
|
+
WireOverride {
|
|
154
|
+
path: vec![key("o")],
|
|
155
|
+
value: json!("second"),
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
// The list carries both decisions.
|
|
160
|
+
assert_eq!(duplicated.len(), 2);
|
|
161
|
+
|
|
162
|
+
// The naive conversion keeps one, without erroring.
|
|
163
|
+
let collapsed: BTreeMap<Vec<PathComponent>, Value> = duplicated
|
|
164
|
+
.iter()
|
|
165
|
+
.map(|entry| (entry.path.clone(), entry.value.clone()))
|
|
166
|
+
.collect();
|
|
167
|
+
assert_eq!(collapsed.len(), 1, "one decision was dropped");
|
|
168
|
+
assert_eq!(
|
|
169
|
+
collapsed[&vec![key("o")]],
|
|
170
|
+
json!("second"),
|
|
171
|
+
"silently, and last-wins"
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// What an API must do instead: detect the duplicate and refuse.
|
|
175
|
+
let mut seen: BTreeMap<Vec<PathComponent>, Value> = BTreeMap::new();
|
|
176
|
+
let mut rejected = false;
|
|
177
|
+
for entry in &duplicated {
|
|
178
|
+
if seen
|
|
179
|
+
.insert(entry.path.clone(), entry.value.clone())
|
|
180
|
+
.is_some()
|
|
181
|
+
{
|
|
182
|
+
rejected = true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
assert!(
|
|
186
|
+
rejected,
|
|
187
|
+
"a duplicate path is detectable, and must be refused"
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// Ordering is not information the wire form may carry.
|
|
192
|
+
///
|
|
193
|
+
/// The in-memory map has one iteration order, sorted structurally. A list has
|
|
194
|
+
/// whatever order the client wrote. Since overlapping override paths are
|
|
195
|
+
/// rejected, no two overrides can affect each other, so order cannot change the
|
|
196
|
+
/// result — and an API must therefore not let a client believe it can.
|
|
197
|
+
#[test]
|
|
198
|
+
fn wire_order_carries_no_meaning() {
|
|
199
|
+
let one = WireOverride {
|
|
200
|
+
path: vec![key("a")],
|
|
201
|
+
value: json!(1),
|
|
202
|
+
};
|
|
203
|
+
let two = WireOverride {
|
|
204
|
+
path: vec![key("b")],
|
|
205
|
+
value: json!(2),
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
let forward: BTreeMap<_, _> = [one.clone(), two.clone()]
|
|
209
|
+
.iter()
|
|
210
|
+
.map(|e| (e.path.clone(), e.value.clone()))
|
|
211
|
+
.collect();
|
|
212
|
+
let reversed: BTreeMap<_, _> = [two, one]
|
|
213
|
+
.iter()
|
|
214
|
+
.map(|e| (e.path.clone(), e.value.clone()))
|
|
215
|
+
.collect();
|
|
216
|
+
|
|
217
|
+
assert_eq!(
|
|
218
|
+
forward, reversed,
|
|
219
|
+
"the map both lists produce is the same, so wire order is not a decision"
|
|
220
|
+
);
|
|
221
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
//! Production-facing evidence for the resource-scoped revision model.
|
|
2
|
+
//!
|
|
3
|
+
//! The [contract](../../../docs/architecture/revision-identity-and-ancestry.md)
|
|
4
|
+
//! proves the 21 clauses; this file proves the scenarios a database has to
|
|
5
|
+
//! survive rather than the clauses a model has to satisfy.
|
|
6
|
+
//!
|
|
7
|
+
//! Three groups: identity, ancestry, and bounded history.
|
|
8
|
+
|
|
9
|
+
use feltdb::state_model::{
|
|
10
|
+
ParentLookup, RetentionPolicy, RevisionError, StateRevision, StateStore,
|
|
11
|
+
};
|
|
12
|
+
use feltdb::{FeltDb, JsonCasResult};
|
|
13
|
+
use serde_json::json;
|
|
14
|
+
use std::sync::Arc;
|
|
15
|
+
use tempfile::TempDir;
|
|
16
|
+
|
|
17
|
+
fn durable(name: &str) -> (TempDir, Arc<FeltDb>, StateStore) {
|
|
18
|
+
let directory = TempDir::new().unwrap();
|
|
19
|
+
let db = Arc::new(FeltDb::open(directory.path().join(name)).unwrap());
|
|
20
|
+
let store = StateStore::with_feltdb(db.clone()).unwrap();
|
|
21
|
+
(directory, db, store)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fn revision_lines(log: &str) -> usize {
|
|
25
|
+
log.lines()
|
|
26
|
+
.filter(|line| line.contains("state:revision:") && !line.contains(r#""deleted":true"#))
|
|
27
|
+
.count()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Identity
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/// Writing the same state twice to one resource records two revisions.
|
|
35
|
+
#[test]
|
|
36
|
+
fn same_content_twice_is_distinct() {
|
|
37
|
+
let (_directory, db, store) = durable("same-twice.log");
|
|
38
|
+
|
|
39
|
+
db.insert("tasks:1", json!({"title": "A"})).unwrap();
|
|
40
|
+
db.update("tasks:1", json!({"title": "A"})).unwrap();
|
|
41
|
+
|
|
42
|
+
let history = store.history_of("tasks:1");
|
|
43
|
+
assert_eq!(history.len(), 2, "two writes, two revisions");
|
|
44
|
+
assert_ne!(history[0].id, history[1].id);
|
|
45
|
+
assert_eq!(
|
|
46
|
+
history[0].content_id, history[1].content_id,
|
|
47
|
+
"and it was the same state both times"
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// A revert is a new historical node, not a return to an old one.
|
|
52
|
+
#[test]
|
|
53
|
+
fn revert_creates_new_revision() {
|
|
54
|
+
let (_directory, db, store) = durable("revert.log");
|
|
55
|
+
|
|
56
|
+
db.insert("tasks:1", json!({"title": "A"})).unwrap();
|
|
57
|
+
db.update("tasks:1", json!({"title": "B"})).unwrap();
|
|
58
|
+
db.update("tasks:1", json!({"title": "A"})).unwrap();
|
|
59
|
+
|
|
60
|
+
let history = store.history_of("tasks:1");
|
|
61
|
+
assert_eq!(history.len(), 3);
|
|
62
|
+
assert_ne!(
|
|
63
|
+
history[0].id, history[2].id,
|
|
64
|
+
"the revert did not resurrect an identity"
|
|
65
|
+
);
|
|
66
|
+
assert_eq!(history[2].parent_id.as_ref(), Some(&history[1].id));
|
|
67
|
+
assert_eq!(history[1].parent_id.as_ref(), Some(&history[0].id));
|
|
68
|
+
assert_eq!(history[0].parent_id, None);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Identical content on two resources is two histories, never one object.
|
|
72
|
+
#[test]
|
|
73
|
+
fn same_content_across_resources_is_distinct() {
|
|
74
|
+
let (_directory, db, store) = durable("two-resources.log");
|
|
75
|
+
|
|
76
|
+
db.insert("tasks:1", json!({"title": "A"})).unwrap();
|
|
77
|
+
db.insert("tasks:2", json!({"title": "A"})).unwrap();
|
|
78
|
+
|
|
79
|
+
let one = store.head_of("tasks:1").unwrap();
|
|
80
|
+
let two = store.head_of("tasks:2").unwrap();
|
|
81
|
+
|
|
82
|
+
assert_ne!(one.id, two.id);
|
|
83
|
+
assert_eq!(one.content_id, two.content_id);
|
|
84
|
+
assert_eq!(one.resource, "tasks:1");
|
|
85
|
+
assert_eq!(two.resource, "tasks:2");
|
|
86
|
+
assert_eq!(store.history_of("tasks:1").len(), 1);
|
|
87
|
+
assert_eq!(store.history_of("tasks:2").len(), 1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Ancestry
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
/// **T6, closed.** No later mint rewrites an existing revision's ancestry.
|
|
95
|
+
#[test]
|
|
96
|
+
fn committed_parent_is_immutable() {
|
|
97
|
+
let (_directory, db, store) = durable("immutable.log");
|
|
98
|
+
|
|
99
|
+
db.insert("tasks:1", json!({"title": "A"})).unwrap();
|
|
100
|
+
db.update("tasks:1", json!({"title": "B"})).unwrap();
|
|
101
|
+
let second = store.head_of("tasks:1").unwrap();
|
|
102
|
+
let recorded_parent = second.parent_id.clone();
|
|
103
|
+
assert!(recorded_parent.is_some());
|
|
104
|
+
|
|
105
|
+
// Everything that rewrote it before: the same content again, the same
|
|
106
|
+
// content as a root, and the same content on another resource.
|
|
107
|
+
db.update("tasks:1", json!({"title": "B"})).unwrap();
|
|
108
|
+
db.insert("tasks:2", json!({"title": "B"})).unwrap();
|
|
109
|
+
assert!(store
|
|
110
|
+
.create("tasks:1", r#"{"title":"B"}"#.into(), "x".into())
|
|
111
|
+
.is_err());
|
|
112
|
+
|
|
113
|
+
assert_eq!(
|
|
114
|
+
store.get(&second.id).unwrap().parent_id,
|
|
115
|
+
recorded_parent,
|
|
116
|
+
"the committed revision kept the parent it was committed with"
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// **T5, closed.** A parent must be an authoritative stored revision.
|
|
121
|
+
#[test]
|
|
122
|
+
fn foreign_parent_is_rejected() {
|
|
123
|
+
let (_directory, _db, store) = durable("foreign.log");
|
|
124
|
+
|
|
125
|
+
let real = store
|
|
126
|
+
.create("tasks:1", r#"{"title":"A"}"#.into(), "x".into())
|
|
127
|
+
.unwrap();
|
|
128
|
+
|
|
129
|
+
// A revision the caller built but never committed.
|
|
130
|
+
let fabricated = StateRevision::at(
|
|
131
|
+
"tasks:1".into(),
|
|
132
|
+
r#"{"never":"stored"}"#.into(),
|
|
133
|
+
None,
|
|
134
|
+
0,
|
|
135
|
+
"x".into(),
|
|
136
|
+
);
|
|
137
|
+
assert!(matches!(
|
|
138
|
+
store.commit(r#"{"title":"B"}"#.into(), &fabricated, "x".into()),
|
|
139
|
+
Err(RevisionError::UnknownParent(_))
|
|
140
|
+
));
|
|
141
|
+
|
|
142
|
+
// And a real revision, but of another resource.
|
|
143
|
+
assert!(matches!(
|
|
144
|
+
store.mint(
|
|
145
|
+
"tasks:2",
|
|
146
|
+
r#"{"title":"B"}"#.into(),
|
|
147
|
+
Some(&real),
|
|
148
|
+
"x".into()
|
|
149
|
+
),
|
|
150
|
+
Err(RevisionError::ForeignParent { .. })
|
|
151
|
+
));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// The fenced path mints from the revision the fence itself committed against,
|
|
155
|
+
/// so no caller-supplied historical object can choose the ancestry.
|
|
156
|
+
#[test]
|
|
157
|
+
fn the_fenced_path_mints_from_the_current_revision() {
|
|
158
|
+
let (_directory, db, store) = durable("fenced.log");
|
|
159
|
+
|
|
160
|
+
db.insert("tasks:1", json!({"title": "A", "__version": 1}))
|
|
161
|
+
.unwrap();
|
|
162
|
+
let before = store.head_of("tasks:1").unwrap();
|
|
163
|
+
|
|
164
|
+
// A refused write mints nothing.
|
|
165
|
+
let refused = db
|
|
166
|
+
.compare_and_set_json("tasks:1", 99, None, None, false, json!({"title": "Z"}))
|
|
167
|
+
.unwrap();
|
|
168
|
+
assert!(matches!(refused, JsonCasResult::VersionConflict { .. }));
|
|
169
|
+
assert_eq!(store.history_of("tasks:1").len(), 1, "no revision minted");
|
|
170
|
+
|
|
171
|
+
// An accepted one descends from what was current.
|
|
172
|
+
db.compare_and_set_json("tasks:1", 1, None, None, false, json!({"title": "B"}))
|
|
173
|
+
.unwrap();
|
|
174
|
+
let after = store.head_of("tasks:1").unwrap();
|
|
175
|
+
assert_eq!(after.parent_id.as_ref(), Some(&before.id));
|
|
176
|
+
assert_eq!(after.sequence, before.sequence + 1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Bounded history
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
/// The whole point of the policy: writing forever does not store forever.
|
|
184
|
+
#[test]
|
|
185
|
+
fn repeated_mutation_has_bounded_history() {
|
|
186
|
+
let (_directory, db, store) = durable("bounded.log");
|
|
187
|
+
store
|
|
188
|
+
.set_retention_policy("tasks:1", RetentionPolicy::keep_last(5))
|
|
189
|
+
.unwrap();
|
|
190
|
+
|
|
191
|
+
db.insert("tasks:1", json!({"n": 0})).unwrap();
|
|
192
|
+
for n in 1..200 {
|
|
193
|
+
db.update("tasks:1", json!({ "n": n })).unwrap();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
assert_eq!(
|
|
197
|
+
store.history_of("tasks:1").len(),
|
|
198
|
+
5,
|
|
199
|
+
"two hundred writes, five retained revisions"
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/// Retention never takes the state the application is using.
|
|
204
|
+
#[test]
|
|
205
|
+
fn current_revision_survives_retention() {
|
|
206
|
+
let (_directory, db, store) = durable("current.log");
|
|
207
|
+
// Even asked for zero, the current revision survives.
|
|
208
|
+
store
|
|
209
|
+
.set_retention_policy("tasks:1", RetentionPolicy::keep_last(0))
|
|
210
|
+
.unwrap();
|
|
211
|
+
|
|
212
|
+
db.insert("tasks:1", json!({"n": 0})).unwrap();
|
|
213
|
+
for n in 1..20 {
|
|
214
|
+
db.update("tasks:1", json!({ "n": n })).unwrap();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let history = store.history_of("tasks:1");
|
|
218
|
+
assert_eq!(history.len(), 1, "keep_last is clamped to at least one");
|
|
219
|
+
assert!(history[0].content.contains(r#""n":19"#));
|
|
220
|
+
|
|
221
|
+
let current: serde_json::Value = db.get("tasks:1").unwrap().unwrap();
|
|
222
|
+
assert_eq!(current["n"], json!(19), "and the row itself is untouched");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// A retained revision's ancestry stays valid, and the cut is legible.
|
|
226
|
+
///
|
|
227
|
+
/// This is the hazard the retention rule was designed around: expiring an
|
|
228
|
+
/// ancestor must not silently leave a retained child pointing at nothing. The
|
|
229
|
+
/// child's `parent_id` is never rewritten — that would break immutability — so
|
|
230
|
+
/// the resource records a horizon, and the oldest survivor reports its parent
|
|
231
|
+
/// as **expired** rather than missing.
|
|
232
|
+
#[test]
|
|
233
|
+
fn parent_survives_required_retention() {
|
|
234
|
+
let (_directory, db, store) = durable("ancestry.log");
|
|
235
|
+
store
|
|
236
|
+
.set_retention_policy("tasks:1", RetentionPolicy::keep_last(3))
|
|
237
|
+
.unwrap();
|
|
238
|
+
|
|
239
|
+
db.insert("tasks:1", json!({"n": 0})).unwrap();
|
|
240
|
+
for n in 1..10 {
|
|
241
|
+
db.update("tasks:1", json!({ "n": n })).unwrap();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let history = store.history_of("tasks:1");
|
|
245
|
+
assert_eq!(history.len(), 3);
|
|
246
|
+
|
|
247
|
+
// Every retained revision but the oldest resolves its parent.
|
|
248
|
+
for revision in &history[1..] {
|
|
249
|
+
assert!(
|
|
250
|
+
matches!(
|
|
251
|
+
store.parent_of(&revision.id),
|
|
252
|
+
Some(ParentLookup::Revision(_))
|
|
253
|
+
),
|
|
254
|
+
"a retained child's parent is retained"
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// The oldest names a parent that was expired on purpose, and says so.
|
|
259
|
+
assert!(matches!(
|
|
260
|
+
store.parent_of(&history[0].id),
|
|
261
|
+
Some(ParentLookup::Expired(_))
|
|
262
|
+
));
|
|
263
|
+
assert_eq!(
|
|
264
|
+
store.retention_horizon("tasks:1"),
|
|
265
|
+
history[0].sequence,
|
|
266
|
+
"the horizon is exactly where history was cut"
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/// Expired revisions are reclaimed, not merely hidden.
|
|
271
|
+
///
|
|
272
|
+
/// Retention removes them from state immediately. The bytes go when the log is
|
|
273
|
+
/// compacted, which is the operation that reclaims space — and this is the one
|
|
274
|
+
/// place the two mechanisms meet: **retention decides what history exists;
|
|
275
|
+
/// compaction reclaims what retention removed.** Neither does the other's job.
|
|
276
|
+
#[test]
|
|
277
|
+
fn expired_history_is_reclaimable() {
|
|
278
|
+
let directory = TempDir::new().unwrap();
|
|
279
|
+
let path = directory.path().join("reclaim.log");
|
|
280
|
+
let db = Arc::new(FeltDb::open(&path).unwrap());
|
|
281
|
+
let store = StateStore::with_feltdb(db.clone()).unwrap();
|
|
282
|
+
|
|
283
|
+
store
|
|
284
|
+
.set_retention_policy("tasks:1", RetentionPolicy::keep_last(2))
|
|
285
|
+
.unwrap();
|
|
286
|
+
db.insert("tasks:1", json!({"n": 0})).unwrap();
|
|
287
|
+
for n in 1..50 {
|
|
288
|
+
db.update("tasks:1", json!({ "n": n })).unwrap();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
assert_eq!(store.history_of("tasks:1").len(), 2, "history is bounded");
|
|
292
|
+
|
|
293
|
+
let versions = db.operation_versions().unwrap();
|
|
294
|
+
db.acknowledge_peer_versions("peer-1".to_string(), versions)
|
|
295
|
+
.unwrap();
|
|
296
|
+
db.compact_operation_log(&["peer-1".to_string()]).unwrap();
|
|
297
|
+
|
|
298
|
+
let log = std::fs::read_to_string(&path).unwrap();
|
|
299
|
+
assert_eq!(
|
|
300
|
+
revision_lines(&log),
|
|
301
|
+
2,
|
|
302
|
+
"compaction wrote out only the revisions retention kept"
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// And the bound holds across a restart.
|
|
306
|
+
drop(store);
|
|
307
|
+
drop(db);
|
|
308
|
+
let reopened = Arc::new(FeltDb::open(&path).unwrap());
|
|
309
|
+
let store = StateStore::with_feltdb(reopened.clone()).unwrap();
|
|
310
|
+
assert_eq!(store.history_of("tasks:1").len(), 2);
|
|
311
|
+
let current: serde_json::Value = reopened.get("tasks:1").unwrap().unwrap();
|
|
312
|
+
assert_eq!(current["n"], json!(49), "current state survived all of it");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/// Minting survives a restart: the head index is derived, not durable, and a
|
|
316
|
+
/// reopened database continues the resource's history rather than restarting it.
|
|
317
|
+
#[test]
|
|
318
|
+
fn history_continues_across_a_restart() {
|
|
319
|
+
let directory = TempDir::new().unwrap();
|
|
320
|
+
let path = directory.path().join("restart.log");
|
|
321
|
+
let db = Arc::new(FeltDb::open(&path).unwrap());
|
|
322
|
+
|
|
323
|
+
db.insert("tasks:1", json!({"n": 0})).unwrap();
|
|
324
|
+
db.update("tasks:1", json!({"n": 1})).unwrap();
|
|
325
|
+
drop(db);
|
|
326
|
+
|
|
327
|
+
let reopened = Arc::new(FeltDb::open(&path).unwrap());
|
|
328
|
+
reopened.update("tasks:1", json!({"n": 2})).unwrap();
|
|
329
|
+
let store = StateStore::with_feltdb(reopened).unwrap();
|
|
330
|
+
|
|
331
|
+
let history = store.history_of("tasks:1");
|
|
332
|
+
assert_eq!(history.len(), 3, "the third write continued the history");
|
|
333
|
+
assert_eq!(
|
|
334
|
+
history[2].sequence, 2,
|
|
335
|
+
"sequence continued, did not restart"
|
|
336
|
+
);
|
|
337
|
+
assert_eq!(history[2].parent_id.as_ref(), Some(&history[1].id));
|
|
338
|
+
}
|