create-feltdb 0.8.8 → 0.9.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/package-versions.js +1 -1
- package/dist/server-source/crates/feltdb/src/lib.rs +343 -1
- package/dist/server-source/crates/feltdb/src/state_model.rs +15 -2
- package/dist/server-source/crates/feltdb-server/src/control_plane.rs +845 -0
- package/dist/server-source/crates/feltdb-server/src/lib.rs +1 -0
- package/dist/server-source/crates/feltdb-server/src/main.rs +1346 -2
- package/dist/server-source/crates/feltdb-server/src/principals.rs +37 -0
- package/package.json +1 -1
package/dist/package-versions.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// One release train keeps generated applications installable. The repository
|
|
2
2
|
// validation script checks these values against every workspace manifest.
|
|
3
|
-
export const FELTDB_PACKAGE_VERSION = '0.
|
|
3
|
+
export const FELTDB_PACKAGE_VERSION = '0.9.0';
|
|
4
4
|
export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;
|
|
@@ -278,7 +278,7 @@ use serde_json::Value;
|
|
|
278
278
|
use sha2::{Digest, Sha256};
|
|
279
279
|
use std::any::type_name;
|
|
280
280
|
use std::collections::hash_map::DefaultHasher;
|
|
281
|
-
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
|
|
281
|
+
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
|
|
282
282
|
use std::fmt::{Display, Formatter};
|
|
283
283
|
use std::fs::{self, OpenOptions};
|
|
284
284
|
use std::hash::{Hash, Hasher};
|
|
@@ -390,6 +390,19 @@ struct Inner {
|
|
|
390
390
|
applied_transactions: HashSet<String>,
|
|
391
391
|
transaction_payload_hashes: HashMap<String, String>,
|
|
392
392
|
transaction_revisions: HashMap<String, (u64, u64)>,
|
|
393
|
+
/// Recent commits, and who asked for them.
|
|
394
|
+
///
|
|
395
|
+
/// Every transaction already records its actor: the audit document written
|
|
396
|
+
/// into the durable log carries `subject`, tenant, application and
|
|
397
|
+
/// revision. Until this existed that document was written and never read
|
|
398
|
+
/// back — replay parsed the record and discarded the audit — so the
|
|
399
|
+
/// database could say *what* changed and never *who changed it*.
|
|
400
|
+
///
|
|
401
|
+
/// Bounded on purpose. This answers "who did this, recently", which is the
|
|
402
|
+
/// question a human asks while operating a running system; it is not an
|
|
403
|
+
/// audit archive, and the durable log remains the record of what happened.
|
|
404
|
+
/// Keeping it bounded is what makes it safe to maintain on the commit path.
|
|
405
|
+
transaction_attributions: VecDeque<TransactionAttribution>,
|
|
393
406
|
collection_cardinality: HashMap<String, u64>,
|
|
394
407
|
/// Derived equality index over `rows`. Never durable, never replicated, and
|
|
395
408
|
/// never consulted for anything but candidate selection: it is maintained
|
|
@@ -737,6 +750,102 @@ pub enum JsonCasResult {
|
|
|
737
750
|
},
|
|
738
751
|
NotFound,
|
|
739
752
|
}
|
|
753
|
+
/// How many recent commits keep their attribution in memory.
|
|
754
|
+
///
|
|
755
|
+
/// Sized to cover what an operator reads in one sitting rather than to be a
|
|
756
|
+
/// log. A larger window costs memory on every instance to answer a question the
|
|
757
|
+
/// durable log already answers more completely.
|
|
758
|
+
const TRANSACTION_ATTRIBUTION_WINDOW: usize = 512;
|
|
759
|
+
|
|
760
|
+
/// How many mutated keys one attribution names before it stops listing them.
|
|
761
|
+
///
|
|
762
|
+
/// A bulk transaction can touch thousands of records; naming them all would
|
|
763
|
+
/// make one commit's attribution larger than the rest of the window. The count
|
|
764
|
+
/// is always exact, so a truncated list is visible as a truncated list.
|
|
765
|
+
const TRANSACTION_ATTRIBUTION_KEYS: usize = 32;
|
|
766
|
+
|
|
767
|
+
/// Who committed a transaction, and what it touched.
|
|
768
|
+
///
|
|
769
|
+
/// Projected from the audit document the transaction already writes. It carries
|
|
770
|
+
/// no record values: attribution answers *who, when, and to what*, and the
|
|
771
|
+
/// values are reachable through the record and revision APIs that authorize
|
|
772
|
+
/// each read.
|
|
773
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
774
|
+
pub struct TransactionAttribution {
|
|
775
|
+
pub transaction_id: String,
|
|
776
|
+
/// The actor the caller authenticated as, when the transaction named one.
|
|
777
|
+
pub subject: Option<String>,
|
|
778
|
+
pub tenant_id: Option<String>,
|
|
779
|
+
pub application_id: Option<String>,
|
|
780
|
+
/// The application contract revision the transaction was executed against.
|
|
781
|
+
pub application_revision: Option<String>,
|
|
782
|
+
pub base_revision: u64,
|
|
783
|
+
pub commit_revision: u64,
|
|
784
|
+
pub state_before: u64,
|
|
785
|
+
pub state_after: u64,
|
|
786
|
+
/// `capability:key` for each mutation, truncated at
|
|
787
|
+
/// [`TRANSACTION_ATTRIBUTION_KEYS`].
|
|
788
|
+
pub keys: Vec<String>,
|
|
789
|
+
/// How many mutations the transaction made, whether or not `keys` lists
|
|
790
|
+
/// them all.
|
|
791
|
+
pub operations: usize,
|
|
792
|
+
/// Milliseconds since the epoch, matching `StoredRow::unix_ms`.
|
|
793
|
+
pub unix_ms: u128,
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
impl TransactionAttribution {
|
|
797
|
+
/// Builds an attribution from a transaction record's own audit document.
|
|
798
|
+
fn from_record(
|
|
799
|
+
transaction_id: &str,
|
|
800
|
+
audit: Option<&Value>,
|
|
801
|
+
base_revision: u64,
|
|
802
|
+
commit_revision: u64,
|
|
803
|
+
state_before: u64,
|
|
804
|
+
state_after: u64,
|
|
805
|
+
rows: &[StoredRow],
|
|
806
|
+
unix_ms: u128,
|
|
807
|
+
) -> Self {
|
|
808
|
+
let text = |field: &str| {
|
|
809
|
+
audit
|
|
810
|
+
.and_then(|value| value.get(field))
|
|
811
|
+
.and_then(Value::as_str)
|
|
812
|
+
.map(str::to_string)
|
|
813
|
+
};
|
|
814
|
+
TransactionAttribution {
|
|
815
|
+
transaction_id: transaction_id.to_string(),
|
|
816
|
+
subject: text("subject"),
|
|
817
|
+
tenant_id: text("tenant"),
|
|
818
|
+
application_id: text("application"),
|
|
819
|
+
application_revision: text("revision"),
|
|
820
|
+
base_revision,
|
|
821
|
+
commit_revision,
|
|
822
|
+
state_before,
|
|
823
|
+
state_after,
|
|
824
|
+
keys: rows
|
|
825
|
+
.iter()
|
|
826
|
+
.take(TRANSACTION_ATTRIBUTION_KEYS)
|
|
827
|
+
.map(|row| format!("{}:{}", row.capability, row.key))
|
|
828
|
+
.collect(),
|
|
829
|
+
operations: rows.len(),
|
|
830
|
+
unix_ms,
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/// Whether this transaction touched a record, as far as `keys` records it.
|
|
835
|
+
pub fn touched(&self, capability: &str, key: &str) -> bool {
|
|
836
|
+
let target = format!("{capability}:{key}");
|
|
837
|
+
self.keys.iter().any(|held| *held == target)
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/// Records an attribution, discarding the oldest once the window is full.
|
|
842
|
+
fn remember_attribution(inner: &mut Inner, attribution: TransactionAttribution) {
|
|
843
|
+
if inner.transaction_attributions.len() >= TRANSACTION_ATTRIBUTION_WINDOW {
|
|
844
|
+
inner.transaction_attributions.pop_front();
|
|
845
|
+
}
|
|
846
|
+
inner.transaction_attributions.push_back(attribution);
|
|
847
|
+
}
|
|
848
|
+
|
|
740
849
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
741
850
|
struct TransactionLogRecord {
|
|
742
851
|
record_type: String,
|
|
@@ -2827,6 +2936,22 @@ impl FeltDb {
|
|
|
2827
2936
|
bookkeeping_nanos += began.elapsed().as_nanos().min(u64::MAX as u128) as u64;
|
|
2828
2937
|
}
|
|
2829
2938
|
append_transaction(&inner.path, &record)?;
|
|
2939
|
+
// Retained from the record that was just made durable, so the
|
|
2940
|
+
// in-memory answer to "who committed this" cannot describe a
|
|
2941
|
+
// transaction the log does not hold.
|
|
2942
|
+
remember_attribution(
|
|
2943
|
+
&mut inner,
|
|
2944
|
+
TransactionAttribution::from_record(
|
|
2945
|
+
transaction_id,
|
|
2946
|
+
record.audit.as_ref(),
|
|
2947
|
+
base_revision,
|
|
2948
|
+
base_revision + 1,
|
|
2949
|
+
state_before,
|
|
2950
|
+
next_sequence,
|
|
2951
|
+
&record.rows,
|
|
2952
|
+
now_ms(),
|
|
2953
|
+
),
|
|
2954
|
+
);
|
|
2830
2955
|
inner.authority_revision = base_revision + 1;
|
|
2831
2956
|
inner.sequence = next_sequence;
|
|
2832
2957
|
for row in &rows {
|
|
@@ -3809,6 +3934,71 @@ impl FeltDb {
|
|
|
3809
3934
|
}
|
|
3810
3935
|
|
|
3811
3936
|
/// Earliest operation still available for incremental replay, by origin.
|
|
3937
|
+
/// Recent commits and who made them, newest first.
|
|
3938
|
+
///
|
|
3939
|
+
/// Bounded by [`TRANSACTION_ATTRIBUTION_WINDOW`]. A transaction older than
|
|
3940
|
+
/// the window is not absent from history — it is in the durable log — it is
|
|
3941
|
+
/// simply no longer answerable from memory, and callers that present this
|
|
3942
|
+
/// to a human must say which of the two they are showing.
|
|
3943
|
+
pub fn recent_transactions(&self, limit: usize) -> Result<Vec<TransactionAttribution>> {
|
|
3944
|
+
let inner = self.state();
|
|
3945
|
+
Ok(inner
|
|
3946
|
+
.transaction_attributions
|
|
3947
|
+
.iter()
|
|
3948
|
+
.rev()
|
|
3949
|
+
.take(limit)
|
|
3950
|
+
.cloned()
|
|
3951
|
+
.collect())
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
/// One transaction's attribution, if it is still inside the window.
|
|
3955
|
+
pub fn transaction_attribution(
|
|
3956
|
+
&self,
|
|
3957
|
+
transaction_id: &str,
|
|
3958
|
+
) -> Result<Option<TransactionAttribution>> {
|
|
3959
|
+
let inner = self.state();
|
|
3960
|
+
Ok(inner
|
|
3961
|
+
.transaction_attributions
|
|
3962
|
+
.iter()
|
|
3963
|
+
.rev()
|
|
3964
|
+
.find(|attribution| attribution.transaction_id == transaction_id)
|
|
3965
|
+
.cloned())
|
|
3966
|
+
}
|
|
3967
|
+
|
|
3968
|
+
/// Recent commits that named this record, newest first.
|
|
3969
|
+
///
|
|
3970
|
+
/// This is how "who last changed this record" is answered without scanning
|
|
3971
|
+
/// the durable log: a transaction names the keys it mutated, so the join is
|
|
3972
|
+
/// over data the commit already recorded.
|
|
3973
|
+
pub fn transactions_touching(
|
|
3974
|
+
&self,
|
|
3975
|
+
capability: &str,
|
|
3976
|
+
key: &str,
|
|
3977
|
+
limit: usize,
|
|
3978
|
+
) -> Result<Vec<TransactionAttribution>> {
|
|
3979
|
+
let inner = self.state();
|
|
3980
|
+
Ok(inner
|
|
3981
|
+
.transaction_attributions
|
|
3982
|
+
.iter()
|
|
3983
|
+
.rev()
|
|
3984
|
+
.filter(|attribution| attribution.touched(capability, key))
|
|
3985
|
+
.take(limit)
|
|
3986
|
+
.cloned()
|
|
3987
|
+
.collect())
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3990
|
+
/// How much of the attribution window is in use, and what it can hold.
|
|
3991
|
+
///
|
|
3992
|
+
/// Reported so a reader can tell an idle system from a window that has
|
|
3993
|
+
/// already rolled over.
|
|
3994
|
+
pub fn transaction_attribution_window(&self) -> Result<(usize, usize)> {
|
|
3995
|
+
let inner = self.state();
|
|
3996
|
+
Ok((
|
|
3997
|
+
inner.transaction_attributions.len(),
|
|
3998
|
+
TRANSACTION_ATTRIBUTION_WINDOW,
|
|
3999
|
+
))
|
|
4000
|
+
}
|
|
4001
|
+
|
|
3812
4002
|
pub fn retained_operation_floors(&self) -> Result<HashMap<String, u64>> {
|
|
3813
4003
|
let inner = self.state();
|
|
3814
4004
|
Ok(inner.change_log.retained_from())
|
|
@@ -4693,6 +4883,24 @@ fn replay_log(inner: &mut Inner) -> Result<()> {
|
|
|
4693
4883
|
.transaction_payload_hashes
|
|
4694
4884
|
.insert(transaction.transaction_id.clone(), hash);
|
|
4695
4885
|
}
|
|
4886
|
+
// Attribution is restored on replay for the same reason it is kept
|
|
4887
|
+
// on commit: an operator who restarts a server must not lose the
|
|
4888
|
+
// answer to "who did this" for work the server already holds.
|
|
4889
|
+
let attribution = TransactionAttribution::from_record(
|
|
4890
|
+
&transaction.transaction_id,
|
|
4891
|
+
transaction.audit.as_ref(),
|
|
4892
|
+
base_revision,
|
|
4893
|
+
inner.authority_revision,
|
|
4894
|
+
transaction.state_before,
|
|
4895
|
+
transaction.state_after,
|
|
4896
|
+
&transaction.rows,
|
|
4897
|
+
transaction
|
|
4898
|
+
.rows
|
|
4899
|
+
.first()
|
|
4900
|
+
.map(|row| row.unix_ms)
|
|
4901
|
+
.unwrap_or_default(),
|
|
4902
|
+
);
|
|
4903
|
+
remember_attribution(inner, attribution);
|
|
4696
4904
|
if inner
|
|
4697
4905
|
.applied_transactions
|
|
4698
4906
|
.insert(transaction.transaction_id)
|
|
@@ -4987,6 +5195,140 @@ mod tests {
|
|
|
4987
5195
|
))
|
|
4988
5196
|
}
|
|
4989
5197
|
|
|
5198
|
+
/// A commit is attributable: the audit document a transaction already
|
|
5199
|
+
/// writes is retained, so a human can ask who made a change.
|
|
5200
|
+
#[test]
|
|
5201
|
+
fn a_committed_transaction_records_who_committed_it() {
|
|
5202
|
+
let path = temp_file();
|
|
5203
|
+
let db = open(&path).expect("open");
|
|
5204
|
+
db.apply_atomic_transaction(
|
|
5205
|
+
"tx_agent_1",
|
|
5206
|
+
None,
|
|
5207
|
+
&[],
|
|
5208
|
+
&[AtomicMutation {
|
|
5209
|
+
capability: "billing:Customer".into(),
|
|
5210
|
+
key: "cus_1".into(),
|
|
5211
|
+
value: Some(serde_json::json!({"name": "Ada", "status": "active"})),
|
|
5212
|
+
}],
|
|
5213
|
+
Some(serde_json::json!({
|
|
5214
|
+
"subject": "agent_data_cleanup",
|
|
5215
|
+
"tenant": "acme",
|
|
5216
|
+
"application": "app_billing",
|
|
5217
|
+
"revision": "rev_7",
|
|
5218
|
+
})),
|
|
5219
|
+
)
|
|
5220
|
+
.expect("commit");
|
|
5221
|
+
|
|
5222
|
+
let recent = db.recent_transactions(10).expect("attributions");
|
|
5223
|
+
assert_eq!(recent.len(), 1);
|
|
5224
|
+
assert_eq!(recent[0].transaction_id, "tx_agent_1");
|
|
5225
|
+
assert_eq!(recent[0].subject.as_deref(), Some("agent_data_cleanup"));
|
|
5226
|
+
assert_eq!(recent[0].application_id.as_deref(), Some("app_billing"));
|
|
5227
|
+
assert_eq!(recent[0].operations, 1);
|
|
5228
|
+
assert_eq!(recent[0].keys, vec!["billing:Customer:cus_1".to_string()]);
|
|
5229
|
+
// Attribution says who and what, never what was written.
|
|
5230
|
+
assert!(!serde_json::to_string(&recent[0]).unwrap().contains("Ada"));
|
|
5231
|
+
|
|
5232
|
+
let touching = db
|
|
5233
|
+
.transactions_touching("billing:Customer", "cus_1", 10)
|
|
5234
|
+
.expect("touching");
|
|
5235
|
+
assert_eq!(touching.len(), 1);
|
|
5236
|
+
assert_eq!(touching[0].transaction_id, "tx_agent_1");
|
|
5237
|
+
assert!(db
|
|
5238
|
+
.transactions_touching("billing:Customer", "cus_2", 10)
|
|
5239
|
+
.expect("touching")
|
|
5240
|
+
.is_empty());
|
|
5241
|
+
|
|
5242
|
+
assert_eq!(
|
|
5243
|
+
db.transaction_attribution("tx_agent_1")
|
|
5244
|
+
.expect("lookup")
|
|
5245
|
+
.map(|value| value.subject)
|
|
5246
|
+
.flatten()
|
|
5247
|
+
.as_deref(),
|
|
5248
|
+
Some("agent_data_cleanup")
|
|
5249
|
+
);
|
|
5250
|
+
|
|
5251
|
+
// Attribution survives a restart, because it is rebuilt from the same
|
|
5252
|
+
// durable records the rest of recovery replays.
|
|
5253
|
+
drop(db);
|
|
5254
|
+
let reopened = open(&path).expect("reopen");
|
|
5255
|
+
let after = reopened.recent_transactions(10).expect("attributions");
|
|
5256
|
+
assert_eq!(after.len(), 1);
|
|
5257
|
+
assert_eq!(after[0].subject.as_deref(), Some("agent_data_cleanup"));
|
|
5258
|
+
assert_eq!(after[0].keys, vec!["billing:Customer:cus_1".to_string()]);
|
|
5259
|
+
drop(reopened);
|
|
5260
|
+
let _ = fs::remove_file(path);
|
|
5261
|
+
}
|
|
5262
|
+
|
|
5263
|
+
/// The window is bounded, and reports that it is.
|
|
5264
|
+
#[test]
|
|
5265
|
+
fn transaction_attribution_is_bounded_and_newest_first() {
|
|
5266
|
+
let path = temp_file();
|
|
5267
|
+
let db = open(&path).expect("open");
|
|
5268
|
+
let commits = TRANSACTION_ATTRIBUTION_WINDOW + 5;
|
|
5269
|
+
for index in 0..commits {
|
|
5270
|
+
db.apply_atomic_transaction(
|
|
5271
|
+
&format!("tx_{index}"),
|
|
5272
|
+
None,
|
|
5273
|
+
&[],
|
|
5274
|
+
&[AtomicMutation {
|
|
5275
|
+
capability: "app:Note".into(),
|
|
5276
|
+
key: format!("note_{index}"),
|
|
5277
|
+
value: Some(serde_json::json!({"index": index})),
|
|
5278
|
+
}],
|
|
5279
|
+
Some(serde_json::json!({"subject": format!("actor_{index}")})),
|
|
5280
|
+
)
|
|
5281
|
+
.expect("commit");
|
|
5282
|
+
}
|
|
5283
|
+
let (held, capacity) = db.transaction_attribution_window().expect("window");
|
|
5284
|
+
assert_eq!(capacity, TRANSACTION_ATTRIBUTION_WINDOW);
|
|
5285
|
+
assert_eq!(held, TRANSACTION_ATTRIBUTION_WINDOW);
|
|
5286
|
+
let recent = db.recent_transactions(3).expect("attributions");
|
|
5287
|
+
assert_eq!(
|
|
5288
|
+
recent.iter().map(|value| value.transaction_id.clone()).collect::<Vec<_>>(),
|
|
5289
|
+
vec![
|
|
5290
|
+
format!("tx_{}", commits - 1),
|
|
5291
|
+
format!("tx_{}", commits - 2),
|
|
5292
|
+
format!("tx_{}", commits - 3),
|
|
5293
|
+
]
|
|
5294
|
+
);
|
|
5295
|
+
// The oldest commits rolled out of the window rather than being kept
|
|
5296
|
+
// forever; the durable log still holds them.
|
|
5297
|
+
assert!(db.transaction_attribution("tx_0").expect("lookup").is_none());
|
|
5298
|
+
drop(db);
|
|
5299
|
+
let _ = fs::remove_file(path);
|
|
5300
|
+
}
|
|
5301
|
+
|
|
5302
|
+
/// A bulk transaction names a bounded sample of keys and an exact count.
|
|
5303
|
+
#[test]
|
|
5304
|
+
fn a_bulk_transaction_truncates_its_key_list_and_keeps_the_count() {
|
|
5305
|
+
let path = temp_file();
|
|
5306
|
+
let db = open(&path).expect("open");
|
|
5307
|
+
let mutations = (0..TRANSACTION_ATTRIBUTION_KEYS + 20)
|
|
5308
|
+
.map(|index| AtomicMutation {
|
|
5309
|
+
capability: "app:Customer".into(),
|
|
5310
|
+
key: format!("cus_{index}"),
|
|
5311
|
+
value: Some(serde_json::json!({"index": index})),
|
|
5312
|
+
})
|
|
5313
|
+
.collect::<Vec<_>>();
|
|
5314
|
+
db.apply_atomic_transaction(
|
|
5315
|
+
"tx_bulk",
|
|
5316
|
+
None,
|
|
5317
|
+
&[],
|
|
5318
|
+
&mutations,
|
|
5319
|
+
Some(serde_json::json!({"subject": "migration_agent"})),
|
|
5320
|
+
)
|
|
5321
|
+
.expect("commit");
|
|
5322
|
+
let attribution = db
|
|
5323
|
+
.transaction_attribution("tx_bulk")
|
|
5324
|
+
.expect("lookup")
|
|
5325
|
+
.expect("held");
|
|
5326
|
+
assert_eq!(attribution.operations, mutations.len());
|
|
5327
|
+
assert_eq!(attribution.keys.len(), TRANSACTION_ATTRIBUTION_KEYS);
|
|
5328
|
+
drop(db);
|
|
5329
|
+
let _ = fs::remove_file(path);
|
|
5330
|
+
}
|
|
5331
|
+
|
|
4990
5332
|
#[test]
|
|
4991
5333
|
fn open_is_zero_init_and_creates_store() {
|
|
4992
5334
|
let path = temp_file();
|
|
@@ -1089,9 +1089,22 @@ pub fn reconcile(
|
|
|
1089
1089
|
/// Proven for this type: durable single-writer append, retrieval by identity
|
|
1090
1090
|
/// across restart, ancestry by parent traversal, and independence between
|
|
1091
1091
|
/// records. **Not** proven and not claimed: multi-writer persistence semantics,
|
|
1092
|
-
/// concurrent revision creation,
|
|
1093
|
-
/// collection, server lifecycle integration, current/head semantics, and
|
|
1092
|
+
/// concurrent revision creation, resource isolation, garbage collection, and
|
|
1094
1093
|
/// distributed replication of revision history.
|
|
1094
|
+
///
|
|
1095
|
+
/// Two entries left that list when `/v1/state/*` began serving this type.
|
|
1096
|
+
/// *Server lifecycle integration*: `feltdb-server` opens a store over its own
|
|
1097
|
+
/// `FeltDb` through [`crate::FeltDBStateSystem`] and serves history, diff,
|
|
1098
|
+
/// classification and reconciliation from it. *Authorization*: those routes are
|
|
1099
|
+
/// authorized like every other route — `state:read` to read, `state:write` to
|
|
1100
|
+
/// commit — which is authorization **at the route**, not in this type. This
|
|
1101
|
+
/// type still has none of its own, and a caller holding a `StateStore` still
|
|
1102
|
+
/// reaches every resource in it.
|
|
1103
|
+
///
|
|
1104
|
+
/// *Current/head semantics* also left, but only partly, and the distinction
|
|
1105
|
+
/// matters: [`head_of`](Self::head_of) answers "the newest retained revision of
|
|
1106
|
+
/// this resource, by sequence". That is not a branch head and there is still no
|
|
1107
|
+
/// ref anything can move.
|
|
1095
1108
|
pub struct StateStore {
|
|
1096
1109
|
backing: Backing,
|
|
1097
1110
|
}
|