@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.
- package/dist/authority-scope.d.ts +2 -0
- package/dist/authority-scope.d.ts.map +1 -1
- package/dist/authority-scope.js +7 -2
- 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 +443 -77
- 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 +14 -7
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +50 -26
- package/dist/deployment.d.ts +2 -0
- 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 +7 -0
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +83 -15
- 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/operation-admission.d.ts +2 -0
- package/dist/operation-admission.d.ts.map +1 -1
- 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-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
|
-
.
|
|
1441
|
-
.
|
|
1442
|
-
.
|
|
1443
|
-
|
|
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(¤t)?;
|
|
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(),
|