@feltdb/core 0.9.0 → 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.
Files changed (105) hide show
  1. package/dist/authority-scope.d.ts +2 -0
  2. package/dist/authority-scope.d.ts.map +1 -1
  3. package/dist/authority-scope.js +7 -2
  4. package/dist/cli/commands.js +19 -21
  5. package/dist/cli/development-session.js +16 -0
  6. package/dist/cli/studio-artifact.js +33 -0
  7. package/dist/create/package-versions.js +1 -1
  8. package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +8 -1
  9. package/dist/create/server-source/crates/feltdb/src/application.rs +16 -0
  10. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +21 -22
  11. package/dist/create/server-source/crates/feltdb/src/lib.rs +451 -33
  12. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +48 -3
  13. package/dist/create/server-source/crates/feltdb/src/state_model.rs +569 -4
  14. package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +1 -0
  15. package/dist/create/server-source/crates/feltdb/src/versioned_object_substrate.rs +708 -0
  16. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +1 -0
  17. package/dist/create/server-source/crates/feltdb/src/workload.rs +480 -44
  18. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_evidence.rs +123 -0
  19. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_substrate_completion.rs +281 -0
  20. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +15 -3
  21. package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +28 -18
  22. package/dist/create/server-source/crates/feltdb/tests/storage_independent_versioned_object_substrate.rs +328 -0
  23. package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +20 -3
  24. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +14 -4
  25. package/dist/create/server-source/crates/feltdb-server/src/backup.rs +0 -4
  26. package/dist/create/server-source/crates/feltdb-server/src/canonical_principals.rs +1093 -0
  27. package/dist/create/server-source/crates/feltdb-server/src/causal.rs +905 -144
  28. package/dist/create/server-source/crates/feltdb-server/src/connections.rs +9 -6
  29. package/dist/create/server-source/crates/feltdb-server/src/control_plane.rs +90 -0
  30. package/dist/create/server-source/crates/feltdb-server/src/lib.rs +1 -0
  31. package/dist/create/server-source/crates/feltdb-server/src/main.rs +443 -77
  32. package/dist/create/server-source/crates/feltdb-server/src/principals.rs +9 -501
  33. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +22 -0
  34. package/dist/create/server-source/crates/feltdb-server/tests/principal_tenant_migration.rs +247 -0
  35. package/dist/db.d.ts +14 -7
  36. package/dist/db.d.ts.map +1 -1
  37. package/dist/db.js +50 -26
  38. package/dist/deployment.d.ts +2 -0
  39. package/dist/deployment.d.ts.map +1 -1
  40. package/dist/deployment.js +4 -2
  41. package/dist/embedded-transaction.d.ts.map +1 -1
  42. package/dist/embedded-transaction.js +4 -0
  43. package/dist/error-codes.d.ts +12 -0
  44. package/dist/error-codes.d.ts.map +1 -1
  45. package/dist/error-codes.js +20 -2
  46. package/dist/feltdb.d.ts +4 -0
  47. package/dist/feltdb.d.ts.map +1 -1
  48. package/dist/file-db.d.ts +10 -0
  49. package/dist/file-db.d.ts.map +1 -1
  50. package/dist/file-db.js +18 -0
  51. package/dist/http-db.d.ts +7 -0
  52. package/dist/http-db.d.ts.map +1 -1
  53. package/dist/http-db.js +83 -15
  54. package/dist/index-core.d.ts +1 -0
  55. package/dist/index-core.d.ts.map +1 -1
  56. package/dist/index-core.js +1 -0
  57. package/dist/operation-admission.d.ts +2 -0
  58. package/dist/operation-admission.d.ts.map +1 -1
  59. package/dist/public-contract.d.ts +112 -0
  60. package/dist/public-contract.d.ts.map +1 -0
  61. package/dist/public-contract.js +125 -0
  62. package/dist/studio/app.d.ts +23 -0
  63. package/dist/studio/app.d.ts.map +1 -1
  64. package/dist/studio/components/ContractInspector.d.ts +5 -0
  65. package/dist/studio/components/ContractInspector.d.ts.map +1 -1
  66. package/dist/studio/components/SettingsPanel.d.ts +4 -1
  67. package/dist/studio/components/SettingsPanel.d.ts.map +1 -1
  68. package/dist/studio/components/index.js +4 -4
  69. package/dist/studio/{components-CehkcX_k.js → components-CJFgdKG6.js} +322 -303
  70. package/dist/studio/control-plane/Data.d.ts.map +1 -1
  71. package/dist/studio/control-plane/Instance.d.ts +10 -0
  72. package/dist/studio/control-plane/Instance.d.ts.map +1 -1
  73. package/dist/studio/control-plane/Operations.d.ts +3 -0
  74. package/dist/studio/control-plane/Operations.d.ts.map +1 -1
  75. package/dist/studio/control-plane/State.d.ts.map +1 -1
  76. package/dist/studio/control-plane/index.d.ts +4 -2
  77. package/dist/studio/control-plane/index.d.ts.map +1 -1
  78. package/dist/studio/control-plane/index.js +2 -2
  79. package/dist/studio/control-plane-DP8ljTDO.js +3207 -0
  80. package/dist/studio/index.js +96 -75
  81. package/dist/studio/utils/api.d.ts +1 -1
  82. package/dist/studio/utils/api.d.ts.map +1 -1
  83. package/dist/studio/utils/control-plane-api.d.ts +12 -1
  84. package/dist/studio/utils/control-plane-api.d.ts.map +1 -1
  85. package/dist/studio/utils/control-plane-api.js +1 -1
  86. package/dist/studio/utils/index.js +16 -16
  87. package/dist/studio/utils/service-api.d.ts +8 -0
  88. package/dist/studio/utils/service-api.d.ts.map +1 -1
  89. package/dist/studio/utils/service-api.js +15 -12
  90. package/dist/studio-app/assets/{feltdb_wasm-DksXDnLa.js → feltdb_wasm-CfNb3IoF.js} +1 -1
  91. package/dist/studio-app/assets/feltdb_wasm_bg-D9d3gY-E.wasm +0 -0
  92. package/dist/studio-app/assets/index-SDXdROi1.js +30 -0
  93. package/dist/studio-app/feltdb-studio.json +6 -0
  94. package/dist/studio-app/index.html +1 -1
  95. package/dist/transaction.d.ts +9 -1
  96. package/dist/transaction.d.ts.map +1 -1
  97. package/dist/transaction.js +6 -2
  98. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  99. package/dist/workspace/local-development-authority.d.ts +8 -0
  100. package/dist/workspace/local-development-authority.d.ts.map +1 -1
  101. package/dist/workspace/local-development-authority.js +136 -1
  102. package/package.json +2 -2
  103. package/dist/studio/control-plane-D6TT2fCQ.js +0 -3064
  104. package/dist/studio-app/assets/feltdb_wasm_bg-Yy4QRHJT.wasm +0 -0
  105. package/dist/studio-app/assets/index-CjELc-ul.js +0 -30
@@ -109,6 +109,142 @@ pub struct StateRevision {
109
109
  pub metadata: BTreeMap<String, Value>,
110
110
  }
111
111
 
112
+ /// A content object addressed by its content identity.
113
+ #[derive(Clone, Debug, Serialize, Deserialize)]
114
+ pub struct StateObject {
115
+ pub id: StateId,
116
+ pub content: String,
117
+ #[serde(default)]
118
+ pub metadata: BTreeMap<String, Value>,
119
+ }
120
+
121
+ impl StateObject {
122
+ pub fn new(content: String) -> Self {
123
+ let id = StateId::compute(&content);
124
+ Self {
125
+ id,
126
+ content,
127
+ metadata: BTreeMap::new(),
128
+ }
129
+ }
130
+
131
+ pub fn verify_integrity(&self) -> bool {
132
+ self.id == StateId::compute(&self.content)
133
+ }
134
+ }
135
+
136
+ /// A mutable named reference to an immutable revision.
137
+ #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
138
+ pub struct StateRef {
139
+ pub namespace: String,
140
+ pub name: String,
141
+ pub target: StateId,
142
+ pub version: u64,
143
+ pub authority: String,
144
+ pub updated_at_ms: u64,
145
+ #[serde(default)]
146
+ pub metadata: BTreeMap<String, Value>,
147
+ }
148
+
149
+ #[derive(Clone, Debug, PartialEq, Eq)]
150
+ pub enum RefError {
151
+ InvalidIdentifier,
152
+ NotFound,
153
+ AlreadyExists,
154
+ UnknownTarget(StateId),
155
+ VersionConflict { expected: u64, current: u64 },
156
+ Storage(String),
157
+ }
158
+
159
+ impl std::fmt::Display for RefError {
160
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161
+ match self {
162
+ RefError::InvalidIdentifier => write!(f, "invalid ref identifier"),
163
+ RefError::NotFound => write!(f, "ref not found"),
164
+ RefError::AlreadyExists => write!(f, "ref already exists"),
165
+ RefError::UnknownTarget(id) => write!(f, "ref target {id} does not exist"),
166
+ RefError::VersionConflict { expected, current } => {
167
+ write!(f, "stale ref version: expected {expected}, current {current}")
168
+ }
169
+ RefError::Storage(message) => write!(f, "{message}"),
170
+ }
171
+ }
172
+ }
173
+
174
+ impl std::error::Error for RefError {}
175
+
176
+ #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
177
+ pub struct MaterializationProjection {
178
+ #[serde(default)]
179
+ pub paths: Vec<Vec<PathComponent>>,
180
+ }
181
+
182
+ impl MaterializationProjection {
183
+ pub fn full() -> Self {
184
+ Self { paths: Vec::new() }
185
+ }
186
+
187
+ pub fn select(paths: Vec<Vec<PathComponent>>) -> Self {
188
+ Self { paths }
189
+ }
190
+
191
+ pub fn is_full(&self) -> bool {
192
+ self.paths.is_empty() || self.paths.iter().any(|path| path.is_empty())
193
+ }
194
+ }
195
+
196
+ #[derive(Clone, Debug, PartialEq, Eq)]
197
+ pub enum MaterializationError {
198
+ RevisionNotFound(StateId),
199
+ ContentUnparseable(String),
200
+ MissingPath(Vec<PathComponent>),
201
+ NonDeterministicProjection,
202
+ }
203
+
204
+ impl std::fmt::Display for MaterializationError {
205
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206
+ match self {
207
+ MaterializationError::RevisionNotFound(id) => write!(f, "revision {id} not found"),
208
+ MaterializationError::ContentUnparseable(error) => {
209
+ write!(f, "revision content is not parseable JSON: {error}")
210
+ }
211
+ MaterializationError::MissingPath(path) => {
212
+ write!(f, "projection path does not resolve: {path:?}")
213
+ }
214
+ MaterializationError::NonDeterministicProjection => write!(
215
+ f,
216
+ "projection contains overlapping paths and does not deterministically resolve"
217
+ ),
218
+ }
219
+ }
220
+ }
221
+
222
+ impl std::error::Error for MaterializationError {}
223
+
224
+ #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
225
+ pub struct GarbageCollectionReport {
226
+ pub roots: Vec<StateRef>,
227
+ pub reachable_revisions: Vec<StateId>,
228
+ pub reachable_objects: Vec<StateId>,
229
+ pub collected_revisions: Vec<StateId>,
230
+ pub collected_objects: Vec<StateId>,
231
+ }
232
+
233
+ #[derive(Clone, Debug, PartialEq, Eq)]
234
+ pub enum GarbageCollectionError {
235
+ Storage(String),
236
+ }
237
+
238
+ impl std::fmt::Display for GarbageCollectionError {
239
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240
+ match self {
241
+ GarbageCollectionError::Storage(message) => write!(f, "{message}"),
242
+ }
243
+ }
244
+ }
245
+
246
+ impl std::error::Error for GarbageCollectionError {}
247
+
112
248
  /// Compute a revision identity.
113
249
  ///
114
250
  /// Every component participates: the same content is a different revision on a
@@ -1011,6 +1147,70 @@ fn assign_path(state: &mut Value, path: &[PathComponent], value: Value) -> Resul
1011
1147
  }
1012
1148
  }
1013
1149
 
1150
+ fn assign_projection(
1151
+ state: &mut Value,
1152
+ path: &[PathComponent],
1153
+ value: Value,
1154
+ ) -> Result<(), MaterializationError> {
1155
+ let Some((last, prefix)) = path.split_last() else {
1156
+ *state = value;
1157
+ return Ok(());
1158
+ };
1159
+
1160
+ let mut current = state;
1161
+ for component in prefix {
1162
+ match component {
1163
+ PathComponent::Key(key) => {
1164
+ if current.is_null() {
1165
+ *current = Value::Object(serde_json::Map::new());
1166
+ }
1167
+ let Some(object) = current.as_object_mut() else {
1168
+ return Err(MaterializationError::MissingPath(path.to_vec()));
1169
+ };
1170
+ current = object.entry(key.clone()).or_insert(Value::Null);
1171
+ }
1172
+ PathComponent::Index(index) => {
1173
+ if current.is_null() {
1174
+ *current = Value::Array(Vec::new());
1175
+ }
1176
+ let Some(array) = current.as_array_mut() else {
1177
+ return Err(MaterializationError::MissingPath(path.to_vec()));
1178
+ };
1179
+ while array.len() <= *index {
1180
+ array.push(Value::Null);
1181
+ }
1182
+ current = &mut array[*index];
1183
+ }
1184
+ }
1185
+ }
1186
+
1187
+ match last {
1188
+ PathComponent::Key(key) => {
1189
+ if current.is_null() {
1190
+ *current = Value::Object(serde_json::Map::new());
1191
+ }
1192
+ let Some(object) = current.as_object_mut() else {
1193
+ return Err(MaterializationError::MissingPath(path.to_vec()));
1194
+ };
1195
+ object.insert(key.clone(), value);
1196
+ Ok(())
1197
+ }
1198
+ PathComponent::Index(index) => {
1199
+ if current.is_null() {
1200
+ *current = Value::Array(Vec::new());
1201
+ }
1202
+ let Some(array) = current.as_array_mut() else {
1203
+ return Err(MaterializationError::MissingPath(path.to_vec()));
1204
+ };
1205
+ while array.len() <= *index {
1206
+ array.push(Value::Null);
1207
+ }
1208
+ array[*index] = value;
1209
+ Ok(())
1210
+ }
1211
+ }
1212
+ }
1213
+
1014
1214
  /// Applies a plan and materializes the result as a new revision.
1015
1215
  ///
1016
1216
  /// The revision is built with the existing [`StateRevision::child`] constructor,
@@ -1107,6 +1307,7 @@ pub fn reconcile(
1107
1307
  /// ref anything can move.
1108
1308
  pub struct StateStore {
1109
1309
  backing: Backing,
1310
+ mutation_lock: Arc<Mutex<()>>,
1110
1311
  }
1111
1312
 
1112
1313
  /// Where revisions actually live.
@@ -1125,6 +1326,8 @@ enum Backing {
1125
1326
  struct VolatileState {
1126
1327
  revisions: HashMap<StateId, StateRevision>,
1127
1328
  retention: HashMap<String, RetentionState>,
1329
+ refs: HashMap<(String, String), StateRef>,
1330
+ objects: HashMap<StateId, StateObject>,
1128
1331
  }
1129
1332
 
1130
1333
  impl Clone for Backing {
@@ -1141,6 +1344,49 @@ fn revision_key(id: &StateId) -> String {
1141
1344
  format!("state:revision:{}", id.as_hex())
1142
1345
  }
1143
1346
 
1347
+ fn object_key(id: &StateId) -> String {
1348
+ format!("state:object:{}", id.as_hex())
1349
+ }
1350
+
1351
+ fn ref_key(namespace: &str, name: &str) -> String {
1352
+ format!("state:ref:{}:{}", encode_ref_part(namespace), encode_ref_part(name))
1353
+ }
1354
+
1355
+ fn encode_ref_part(value: &str) -> String {
1356
+ value.replace('%', "%25").replace(':', "%3A")
1357
+ }
1358
+
1359
+ fn valid_ref_component(value: &str) -> bool {
1360
+ !value.trim().is_empty()
1361
+ }
1362
+
1363
+ fn looks_like_state_id(value: &str) -> bool {
1364
+ value.len() == 64 && value.chars().all(|c| c.is_ascii_hexdigit())
1365
+ }
1366
+
1367
+ fn current_time_ms() -> u64 {
1368
+ std::time::SystemTime::now()
1369
+ .duration_since(std::time::UNIX_EPOCH)
1370
+ .unwrap_or_default()
1371
+ .as_millis() as u64
1372
+ }
1373
+
1374
+ fn collect_referenced_objects(revision: &StateRevision) -> BTreeSet<StateId> {
1375
+ let mut ids = BTreeSet::from([revision.content_id.clone()]);
1376
+ if let Some(values) = revision
1377
+ .metadata
1378
+ .get("object_refs")
1379
+ .and_then(Value::as_array)
1380
+ {
1381
+ for value in values {
1382
+ if let Some(hex) = value.as_str().filter(|value| looks_like_state_id(value)) {
1383
+ ids.insert(StateId::from_hex(hex.to_string()));
1384
+ }
1385
+ }
1386
+ }
1387
+ ids
1388
+ }
1389
+
1144
1390
  /// Where a resource's retention watermark lives.
1145
1391
  fn retention_key(resource: &str) -> String {
1146
1392
  format!("_retention:{resource}")
@@ -1311,6 +1557,7 @@ impl StateStore {
1311
1557
  pub fn new_volatile() -> Self {
1312
1558
  StateStore {
1313
1559
  backing: Backing::Volatile(Arc::new(Mutex::new(VolatileState::default()))),
1560
+ mutation_lock: Arc::new(Mutex::new(())),
1314
1561
  }
1315
1562
  }
1316
1563
 
@@ -1326,6 +1573,7 @@ impl StateStore {
1326
1573
  pub fn with_feltdb(feltdb: Arc<crate::FeltDb>) -> Result<Self, String> {
1327
1574
  Ok(StateStore {
1328
1575
  backing: Backing::Durable(feltdb),
1576
+ mutation_lock: Arc::new(Mutex::new(())),
1329
1577
  })
1330
1578
  }
1331
1579
 
@@ -1436,13 +1684,35 @@ impl StateStore {
1436
1684
  }
1437
1685
  match &self.backing {
1438
1686
  Backing::Volatile(state) => {
1687
+ let mut state = state.lock().unwrap();
1439
1688
  state
1440
- .lock()
1441
- .unwrap()
1442
- .revisions
1443
- .insert(revision.id.clone(), revision.clone());
1689
+ .objects
1690
+ .entry(revision.content_id.clone())
1691
+ .or_insert_with(|| StateObject::new(revision.content.clone()));
1692
+ state.revisions.insert(revision.id.clone(), revision.clone());
1444
1693
  }
1445
1694
  Backing::Durable(feltdb) => {
1695
+ let object = StateObject::new(revision.content.clone());
1696
+ if let Some(existing) = feltdb
1697
+ .get::<StateObject>(&object_key(&object.id))
1698
+ .map_err(|error| {
1699
+ RevisionError::Storage(format!("Failed to read state object: {error}"))
1700
+ })?
1701
+ {
1702
+ if !existing.verify_integrity() {
1703
+ return Err(RevisionError::Storage(
1704
+ "Stored state object failed integrity verification".to_string(),
1705
+ ));
1706
+ }
1707
+ } else {
1708
+ feltdb
1709
+ .insert(&object_key(&object.id), object)
1710
+ .map_err(|error| {
1711
+ RevisionError::Storage(format!(
1712
+ "Failed to persist state object to FeltDB: {error}"
1713
+ ))
1714
+ })?;
1715
+ }
1446
1716
  feltdb
1447
1717
  .insert(&revision_key(&revision.id), revision.clone())
1448
1718
  .map_err(|error| {
@@ -1545,6 +1815,273 @@ impl StateStore {
1545
1815
  names
1546
1816
  }
1547
1817
 
1818
+ /// Create a mutable reference in a namespace.
1819
+ pub fn create_ref(
1820
+ &self,
1821
+ namespace: &str,
1822
+ name: &str,
1823
+ target: StateId,
1824
+ authority: String,
1825
+ metadata: BTreeMap<String, Value>,
1826
+ ) -> Result<StateRef, RefError> {
1827
+ if !valid_ref_component(namespace) || !valid_ref_component(name) {
1828
+ return Err(RefError::InvalidIdentifier);
1829
+ }
1830
+ if !self.exists(&target) {
1831
+ return Err(RefError::UnknownTarget(target));
1832
+ }
1833
+ let _guard = self.mutation_lock.lock().unwrap();
1834
+ if self.get_ref(namespace, name).is_some() {
1835
+ return Err(RefError::AlreadyExists);
1836
+ }
1837
+ let reference = StateRef {
1838
+ namespace: namespace.to_string(),
1839
+ name: name.to_string(),
1840
+ target,
1841
+ version: 1,
1842
+ authority,
1843
+ updated_at_ms: current_time_ms(),
1844
+ metadata,
1845
+ };
1846
+ self.write_ref(&reference)?;
1847
+ Ok(reference)
1848
+ }
1849
+
1850
+ /// Read a reference.
1851
+ pub fn get_ref(&self, namespace: &str, name: &str) -> Option<StateRef> {
1852
+ if !valid_ref_component(namespace) || !valid_ref_component(name) {
1853
+ return None;
1854
+ }
1855
+ match &self.backing {
1856
+ Backing::Volatile(state) => state
1857
+ .lock()
1858
+ .unwrap()
1859
+ .refs
1860
+ .get(&(namespace.to_string(), name.to_string()))
1861
+ .cloned(),
1862
+ Backing::Durable(feltdb) => feltdb
1863
+ .get::<StateRef>(&ref_key(namespace, name))
1864
+ .ok()
1865
+ .flatten(),
1866
+ }
1867
+ }
1868
+
1869
+ /// Move a reference with stale-version rejection.
1870
+ pub fn move_ref(
1871
+ &self,
1872
+ namespace: &str,
1873
+ name: &str,
1874
+ target: StateId,
1875
+ authority: String,
1876
+ ) -> Result<StateRef, RefError> {
1877
+ let current = self.get_ref(namespace, name).ok_or(RefError::NotFound)?;
1878
+ self.compare_and_swap_ref(namespace, name, current.version, target, authority, None)
1879
+ }
1880
+
1881
+ /// Compare-and-swap move for a reference.
1882
+ pub fn compare_and_swap_ref(
1883
+ &self,
1884
+ namespace: &str,
1885
+ name: &str,
1886
+ expected_version: u64,
1887
+ target: StateId,
1888
+ authority: String,
1889
+ metadata: Option<BTreeMap<String, Value>>,
1890
+ ) -> Result<StateRef, RefError> {
1891
+ if !valid_ref_component(namespace) || !valid_ref_component(name) {
1892
+ return Err(RefError::InvalidIdentifier);
1893
+ }
1894
+ if !self.exists(&target) {
1895
+ return Err(RefError::UnknownTarget(target));
1896
+ }
1897
+ let _guard = self.mutation_lock.lock().unwrap();
1898
+ let mut current = self.get_ref(namespace, name).ok_or(RefError::NotFound)?;
1899
+ if current.version != expected_version {
1900
+ return Err(RefError::VersionConflict {
1901
+ expected: expected_version,
1902
+ current: current.version,
1903
+ });
1904
+ }
1905
+ current.target = target;
1906
+ current.version += 1;
1907
+ current.authority = authority;
1908
+ current.updated_at_ms = current_time_ms();
1909
+ if let Some(metadata) = metadata {
1910
+ current.metadata = metadata;
1911
+ }
1912
+ self.write_ref(&current)?;
1913
+ Ok(current)
1914
+ }
1915
+
1916
+ /// Delete a reference, optionally enforcing the current version.
1917
+ pub fn delete_ref(
1918
+ &self,
1919
+ namespace: &str,
1920
+ name: &str,
1921
+ expected_version: Option<u64>,
1922
+ ) -> Result<(), RefError> {
1923
+ if !valid_ref_component(namespace) || !valid_ref_component(name) {
1924
+ return Err(RefError::InvalidIdentifier);
1925
+ }
1926
+ let _guard = self.mutation_lock.lock().unwrap();
1927
+ let current = self.get_ref(namespace, name).ok_or(RefError::NotFound)?;
1928
+ if let Some(expected) = expected_version {
1929
+ if current.version != expected {
1930
+ return Err(RefError::VersionConflict {
1931
+ expected,
1932
+ current: current.version,
1933
+ });
1934
+ }
1935
+ }
1936
+ self.remove_ref(namespace, name)
1937
+ }
1938
+
1939
+ /// Enumerate refs, optionally scoped to one namespace.
1940
+ pub fn list_refs(&self, namespace: Option<&str>) -> Vec<StateRef> {
1941
+ let mut refs: Vec<StateRef> = match &self.backing {
1942
+ Backing::Volatile(state) => state.lock().unwrap().refs.values().cloned().collect(),
1943
+ Backing::Durable(feltdb) => feltdb
1944
+ .list_collection("state")
1945
+ .unwrap_or_default()
1946
+ .into_iter()
1947
+ .filter(|row| row.key.starts_with("state:ref:") && !row.deleted)
1948
+ .filter_map(|row| serde_json::from_value::<StateRef>(row.value).ok())
1949
+ .collect(),
1950
+ };
1951
+ if let Some(namespace) = namespace {
1952
+ refs.retain(|reference| reference.namespace == namespace);
1953
+ }
1954
+ refs.sort_by(|one, two| {
1955
+ one.namespace
1956
+ .cmp(&two.namespace)
1957
+ .then_with(|| one.name.cmp(&two.name))
1958
+ });
1959
+ refs
1960
+ }
1961
+
1962
+ /// Materialize a full revision value or a deterministic projection of it.
1963
+ pub fn materialize(
1964
+ &self,
1965
+ revision_id: &StateId,
1966
+ projection: &MaterializationProjection,
1967
+ ) -> Result<Value, MaterializationError> {
1968
+ let revision = self
1969
+ .get(revision_id)
1970
+ .ok_or_else(|| MaterializationError::RevisionNotFound(revision_id.clone()))?;
1971
+ let source: Value = serde_json::from_str(&revision.content)
1972
+ .map_err(|error| MaterializationError::ContentUnparseable(error.to_string()))?;
1973
+ if projection.is_full() {
1974
+ return Ok(source);
1975
+ }
1976
+ let mut paths = projection.paths.clone();
1977
+ paths.sort();
1978
+ paths.dedup();
1979
+ for pair in paths.windows(2) {
1980
+ if paths_overlap(&pair[0], &pair[1]) {
1981
+ return Err(MaterializationError::NonDeterministicProjection);
1982
+ }
1983
+ }
1984
+ let mut projected = Value::Object(serde_json::Map::new());
1985
+ for path in paths {
1986
+ let value = resolve_path(&source, &path)
1987
+ .cloned()
1988
+ .ok_or_else(|| MaterializationError::MissingPath(path.clone()))?;
1989
+ assign_projection(&mut projected, &path, value)?;
1990
+ }
1991
+ Ok(projected)
1992
+ }
1993
+
1994
+ /// Collect revisions and objects unreachable from current refs.
1995
+ pub fn collect_unreachable(&self) -> Result<GarbageCollectionReport, GarbageCollectionError> {
1996
+ let _guard = self.mutation_lock.lock().unwrap();
1997
+ let roots = self.list_refs(None);
1998
+ let mut reachable_revisions = BTreeSet::new();
1999
+ let mut reachable_objects = BTreeSet::new();
2000
+ let mut pending: Vec<StateId> = roots.iter().map(|reference| reference.target.clone()).collect();
2001
+ while let Some(id) = pending.pop() {
2002
+ if !reachable_revisions.insert(id.clone()) {
2003
+ continue;
2004
+ }
2005
+ let Some(revision) = self.get(&id) else {
2006
+ continue;
2007
+ };
2008
+ if let Some(parent_id) = revision.parent_id.clone() {
2009
+ pending.push(parent_id);
2010
+ }
2011
+ for object_id in collect_referenced_objects(&revision) {
2012
+ reachable_objects.insert(object_id);
2013
+ }
2014
+ }
2015
+
2016
+ let mut all_revisions: Vec<StateId> = self.all_revisions().into_iter().map(|r| r.id).collect();
2017
+ all_revisions.sort();
2018
+ let mut collected_revisions = Vec::new();
2019
+ for revision_id in all_revisions {
2020
+ if !reachable_revisions.contains(&revision_id) {
2021
+ self.remove(&revision_id)
2022
+ .map_err(|error| GarbageCollectionError::Storage(error.to_string()))?;
2023
+ collected_revisions.push(revision_id);
2024
+ }
2025
+ }
2026
+
2027
+ let mut all_objects: Vec<StateId> = self.all_objects().into_iter().map(|o| o.id).collect();
2028
+ all_objects.sort();
2029
+ let mut collected_objects = Vec::new();
2030
+ for object_id in all_objects {
2031
+ if !reachable_objects.contains(&object_id) {
2032
+ self.remove_object(&object_id)?;
2033
+ collected_objects.push(object_id);
2034
+ }
2035
+ }
2036
+
2037
+ let mut reachable_revisions_out: Vec<_> = reachable_revisions.into_iter().collect();
2038
+ reachable_revisions_out.sort();
2039
+ let mut reachable_objects_out: Vec<_> = reachable_objects.into_iter().collect();
2040
+ reachable_objects_out.sort();
2041
+ Ok(GarbageCollectionReport {
2042
+ roots,
2043
+ reachable_revisions: reachable_revisions_out,
2044
+ reachable_objects: reachable_objects_out,
2045
+ collected_revisions,
2046
+ collected_objects,
2047
+ })
2048
+ }
2049
+
2050
+ fn write_ref(&self, reference: &StateRef) -> Result<(), RefError> {
2051
+ match &self.backing {
2052
+ Backing::Volatile(state) => {
2053
+ state.lock().unwrap().refs.insert(
2054
+ (reference.namespace.clone(), reference.name.clone()),
2055
+ reference.clone(),
2056
+ );
2057
+ }
2058
+ Backing::Durable(feltdb) => {
2059
+ feltdb
2060
+ .insert(&ref_key(&reference.namespace, &reference.name), reference.clone())
2061
+ .map_err(|error| RefError::Storage(error.to_string()))?;
2062
+ }
2063
+ }
2064
+ Ok(())
2065
+ }
2066
+
2067
+ fn remove_ref(&self, namespace: &str, name: &str) -> Result<(), RefError> {
2068
+ match &self.backing {
2069
+ Backing::Volatile(state) => {
2070
+ state
2071
+ .lock()
2072
+ .unwrap()
2073
+ .refs
2074
+ .remove(&(namespace.to_string(), name.to_string()));
2075
+ }
2076
+ Backing::Durable(feltdb) => {
2077
+ feltdb
2078
+ .delete(&ref_key(namespace, name))
2079
+ .map_err(|error| RefError::Storage(error.to_string()))?;
2080
+ }
2081
+ }
2082
+ Ok(())
2083
+ }
2084
+
1548
2085
  fn all_revisions(&self) -> Vec<StateRevision> {
1549
2086
  match &self.backing {
1550
2087
  Backing::Volatile(state) => state.lock().unwrap().revisions.values().cloned().collect(),
@@ -1558,6 +2095,19 @@ impl StateStore {
1558
2095
  }
1559
2096
  }
1560
2097
 
2098
+ fn all_objects(&self) -> Vec<StateObject> {
2099
+ match &self.backing {
2100
+ Backing::Volatile(state) => state.lock().unwrap().objects.values().cloned().collect(),
2101
+ Backing::Durable(feltdb) => feltdb
2102
+ .list_collection("state")
2103
+ .unwrap_or_default()
2104
+ .into_iter()
2105
+ .filter(|row| row.key.starts_with("state:object:") && !row.deleted)
2106
+ .filter_map(|row| serde_json::from_value::<StateObject>(row.value).ok())
2107
+ .collect(),
2108
+ }
2109
+ }
2110
+
1561
2111
  /// A digest of this store's **history**: the revision graph and retention
1562
2112
  /// state, and nothing else.
1563
2113
  ///
@@ -1684,6 +2234,20 @@ impl StateStore {
1684
2234
  Ok(())
1685
2235
  }
1686
2236
 
2237
+ fn remove_object(&self, id: &StateId) -> Result<(), GarbageCollectionError> {
2238
+ match &self.backing {
2239
+ Backing::Volatile(state) => {
2240
+ state.lock().unwrap().objects.remove(id);
2241
+ }
2242
+ Backing::Durable(feltdb) => {
2243
+ feltdb
2244
+ .delete(&object_key(id))
2245
+ .map_err(|error| GarbageCollectionError::Storage(error.to_string()))?;
2246
+ }
2247
+ }
2248
+ Ok(())
2249
+ }
2250
+
1687
2251
  fn retention_state(&self, resource: &str) -> RetentionState {
1688
2252
  match &self.backing {
1689
2253
  Backing::Volatile(state) => state
@@ -1732,6 +2296,7 @@ impl Clone for StateStore {
1732
2296
  fn clone(&self) -> Self {
1733
2297
  StateStore {
1734
2298
  backing: self.backing.clone(),
2299
+ mutation_lock: self.mutation_lock.clone(),
1735
2300
  }
1736
2301
  }
1737
2302
  }
@@ -288,6 +288,7 @@ fn create_workload(trigger: &StateTrigger, operation: &Operation, now: i64) -> C
288
288
  "record": operation.value,
289
289
  }),
290
290
  requested_by: trigger.requested_by.clone(),
291
+ session_id: None,
291
292
  authorization_snapshot: trigger.authorization_snapshot.clone(),
292
293
  capability_snapshot,
293
294
  connection_scopes: trigger.connection_scopes.clone(),