@feltdb/core 0.7.1 → 0.7.3
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/README.md +22 -0
- package/dist/cli/commands.js +163 -33
- package/dist/cli/index.js +1 -1
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +2 -1
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +769 -0
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +305 -0
- package/dist/create/server-source/crates/feltdb/src/lib.rs +9 -0
- package/dist/create/server-source/crates/feltdb/src/state_facade.rs +292 -0
- package/dist/create/server-source/crates/feltdb/src/state_model.rs +1856 -0
- package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +634 -0
- package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +366 -0
- package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +270 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +13 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +184 -1
- package/dist/db.d.ts +7 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +12 -1
- package/dist/feltdb.d.ts +2 -0
- package/dist/feltdb.d.ts.map +1 -1
- package/dist/http-db.d.ts +24 -0
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +13 -0
- package/dist/index-core.d.ts +1 -0
- package/dist/index-core.d.ts.map +1 -1
- package/dist/studio/app.d.ts +3 -1
- package/dist/studio/app.d.ts.map +1 -1
- package/dist/studio/components/ApplicationDesigner.d.ts +9 -1
- package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
- package/dist/studio/components/index.js +1 -1
- package/dist/studio/{components-C5p2TfIU.js → components-Dxuhrv8_.js} +196 -187
- package/dist/studio/index.js +66 -65
- package/dist/studio-app/assets/{feltdb_wasm-C9xpYtna.js → feltdb_wasm-DVKsw75S.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-DNyNf0yy.wasm +0 -0
- package/dist/studio-app/assets/index-B5tnmvSD.js +28 -0
- package/dist/studio-app/index.html +1 -1
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/package.json +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-BsXHw7eX.wasm +0 -0
- package/dist/studio-app/assets/index-sQyf4Ewl.js +0 -28
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
//! Integration Smoke Test: End-to-End State Model Functionality
|
|
2
|
+
//!
|
|
3
|
+
//! This test demonstrates complete state model usage without Git dependencies:
|
|
4
|
+
//! 1. Create initial state
|
|
5
|
+
//! 2. Branch state
|
|
6
|
+
//! 3. Make changes on each branch
|
|
7
|
+
//! 4. Inspect topology
|
|
8
|
+
//! 5. Compute semantic diffs
|
|
9
|
+
//! 6. Classify conflicts
|
|
10
|
+
//! 7. Supply reconciliation result
|
|
11
|
+
//! 8. Materialize reconciled state
|
|
12
|
+
//! 9. Verify restart recovery
|
|
13
|
+
|
|
14
|
+
#[cfg(test)]
|
|
15
|
+
mod integration_smoke_test {
|
|
16
|
+
use feltdb::{
|
|
17
|
+
StateRevision, StateStore, StateTopology, Relationship, SemanticDiff,
|
|
18
|
+
ConflictClassification, ConflictClass, ReconciliationPlan,
|
|
19
|
+
};
|
|
20
|
+
use serde_json::json;
|
|
21
|
+
|
|
22
|
+
#[test]
|
|
23
|
+
fn end_to_end_state_model_smoke_test() {
|
|
24
|
+
// Step 1: Create initial application state
|
|
25
|
+
let store = StateStore::new_volatile();
|
|
26
|
+
let initial_state = store
|
|
27
|
+
.create(
|
|
28
|
+
r#"{"users":{"alice":{"balance":100},"bob":{"balance":50}}}"#.to_string(),
|
|
29
|
+
"initial-authority".to_string(),
|
|
30
|
+
)
|
|
31
|
+
.expect("Failed to create initial state");
|
|
32
|
+
|
|
33
|
+
assert!(store.exists(&initial_state.id));
|
|
34
|
+
assert_eq!(store.current().unwrap().id, initial_state.id);
|
|
35
|
+
|
|
36
|
+
println!("✓ Step 1: Created initial state ({})", initial_state.id);
|
|
37
|
+
|
|
38
|
+
// Step 2: Create first branch (Alice's branch - modifies alice balance)
|
|
39
|
+
let alice_branch = store
|
|
40
|
+
.commit(
|
|
41
|
+
r#"{"users":{"alice":{"balance":150},"bob":{"balance":50}}}"#.to_string(),
|
|
42
|
+
&initial_state,
|
|
43
|
+
"alice-authority".to_string(),
|
|
44
|
+
)
|
|
45
|
+
.expect("Failed to create alice branch");
|
|
46
|
+
|
|
47
|
+
store
|
|
48
|
+
.create_branch("alice-branch".to_string(), alice_branch.id.clone())
|
|
49
|
+
.expect("Failed to create alice-branch");
|
|
50
|
+
|
|
51
|
+
println!(
|
|
52
|
+
"✓ Step 2: Created alice branch ({}), parent: {}",
|
|
53
|
+
alice_branch.id, alice_branch.parent().unwrap()
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Step 3: Create second branch from initial state (Bob's branch - modifies bob balance)
|
|
57
|
+
let bob_branch = store
|
|
58
|
+
.commit(
|
|
59
|
+
r#"{"users":{"alice":{"balance":100},"bob":{"balance":150}}}"#.to_string(),
|
|
60
|
+
&initial_state,
|
|
61
|
+
"bob-authority".to_string(),
|
|
62
|
+
)
|
|
63
|
+
.expect("Failed to create bob branch");
|
|
64
|
+
|
|
65
|
+
store
|
|
66
|
+
.create_branch("bob-branch".to_string(), bob_branch.id.clone())
|
|
67
|
+
.expect("Failed to create bob-branch");
|
|
68
|
+
|
|
69
|
+
println!(
|
|
70
|
+
"✓ Step 3: Created bob branch ({}), parent: {}",
|
|
71
|
+
bob_branch.id, bob_branch.parent().unwrap()
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Step 4: Inspect topology
|
|
75
|
+
let mut topology = StateTopology::new();
|
|
76
|
+
topology.add_revision(initial_state.clone());
|
|
77
|
+
topology.add_revision(alice_branch.clone());
|
|
78
|
+
topology.add_revision(bob_branch.clone());
|
|
79
|
+
|
|
80
|
+
// Verify ancestry relationships
|
|
81
|
+
assert!(topology.is_ancestor(&initial_state.id, &alice_branch.id));
|
|
82
|
+
assert!(topology.is_ancestor(&initial_state.id, &bob_branch.id));
|
|
83
|
+
assert!(!topology.is_ancestor(&alice_branch.id, &bob_branch.id));
|
|
84
|
+
assert!(!topology.is_ancestor(&bob_branch.id, &alice_branch.id));
|
|
85
|
+
|
|
86
|
+
let common = topology.common_ancestor(&alice_branch.id, &bob_branch.id);
|
|
87
|
+
assert_eq!(common, Some(initial_state.id.clone()));
|
|
88
|
+
|
|
89
|
+
let relationship = topology.relationship(&alice_branch.id, &bob_branch.id);
|
|
90
|
+
assert_eq!(relationship, Relationship::Diverged);
|
|
91
|
+
|
|
92
|
+
println!("✓ Step 4: Verified topology");
|
|
93
|
+
println!(
|
|
94
|
+
" - Alice and Bob branches diverged from {}",
|
|
95
|
+
initial_state.id
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Step 5: Compute semantic diffs
|
|
99
|
+
let initial_json = serde_json::from_str(
|
|
100
|
+
r#"{"users":{"alice":{"balance":100},"bob":{"balance":50}}}"#,
|
|
101
|
+
)
|
|
102
|
+
.unwrap();
|
|
103
|
+
let alice_json = serde_json::from_str(
|
|
104
|
+
r#"{"users":{"alice":{"balance":150},"bob":{"balance":50}}}"#,
|
|
105
|
+
)
|
|
106
|
+
.unwrap();
|
|
107
|
+
let bob_json = serde_json::from_str(
|
|
108
|
+
r#"{"users":{"alice":{"balance":100},"bob":{"balance":150}}}"#,
|
|
109
|
+
)
|
|
110
|
+
.unwrap();
|
|
111
|
+
|
|
112
|
+
let alice_diff = SemanticDiff::compute(&initial_json, &alice_json);
|
|
113
|
+
let bob_diff = SemanticDiff::compute(&initial_json, &bob_json);
|
|
114
|
+
|
|
115
|
+
assert_eq!(alice_diff.changes.len(), 1);
|
|
116
|
+
assert_eq!(bob_diff.changes.len(), 1);
|
|
117
|
+
|
|
118
|
+
println!("✓ Step 5: Computed semantic diffs");
|
|
119
|
+
println!(" - Alice's change: {:?}", alice_diff.changes[0]);
|
|
120
|
+
println!(" - Bob's change: {:?}", bob_diff.changes[0]);
|
|
121
|
+
|
|
122
|
+
// Step 6: Classify conflicts
|
|
123
|
+
let conflict_class = ConflictClassification::classify(
|
|
124
|
+
&initial_state,
|
|
125
|
+
&alice_branch,
|
|
126
|
+
&bob_branch,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// These changes are independent (different paths)
|
|
130
|
+
assert_eq!(conflict_class.overall, ConflictClass::Independent);
|
|
131
|
+
assert!(
|
|
132
|
+
conflict_class.path_conflicts.iter()
|
|
133
|
+
.all(|c| c.classification == ConflictClass::Independent)
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
println!("✓ Step 6: Classified conflicts as INDEPENDENT");
|
|
137
|
+
|
|
138
|
+
// Step 7: Create reconciliation plan with explicit parent choice
|
|
139
|
+
let plan = ReconciliationPlan::new(
|
|
140
|
+
alice_branch.id.clone(),
|
|
141
|
+
bob_branch.id.clone(),
|
|
142
|
+
initial_state.id.clone(),
|
|
143
|
+
true, // Choose alice's state as parent
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
assert!(plan.validate());
|
|
147
|
+
|
|
148
|
+
println!("✓ Step 7: Created reconciliation plan (parent_choice=alice)");
|
|
149
|
+
|
|
150
|
+
// Step 8: Materialize reconciled state
|
|
151
|
+
// For this test, merge by combining both changes
|
|
152
|
+
let merged_json = json!({
|
|
153
|
+
"users": {
|
|
154
|
+
"alice": {"balance": 150},
|
|
155
|
+
"bob": {"balance": 150}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
let merged_content = merged_json.to_string();
|
|
159
|
+
|
|
160
|
+
let reconciled = StateRevision::child(
|
|
161
|
+
merged_content,
|
|
162
|
+
&alice_branch,
|
|
163
|
+
"reconciliation-authority".to_string(),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Store the reconciled state
|
|
167
|
+
let final_state = store
|
|
168
|
+
.commit(
|
|
169
|
+
reconciled.content.clone(),
|
|
170
|
+
&alice_branch,
|
|
171
|
+
"reconciliation-authority".to_string(),
|
|
172
|
+
)
|
|
173
|
+
.expect("Failed to commit reconciled state");
|
|
174
|
+
|
|
175
|
+
println!("✓ Step 8: Materialized reconciled state ({})", final_state.id);
|
|
176
|
+
|
|
177
|
+
// Step 9: Verify state properties
|
|
178
|
+
let retrieved = store
|
|
179
|
+
.get(&final_state.id)
|
|
180
|
+
.expect("Failed to retrieve final state");
|
|
181
|
+
|
|
182
|
+
assert_eq!(retrieved.id, final_state.id);
|
|
183
|
+
assert_eq!(retrieved.authority, "reconciliation-authority");
|
|
184
|
+
assert!(retrieved.verify_integrity());
|
|
185
|
+
|
|
186
|
+
// Verify we can recover parent
|
|
187
|
+
let parent = store
|
|
188
|
+
.get(retrieved.parent().unwrap())
|
|
189
|
+
.expect("Failed to get parent");
|
|
190
|
+
assert_eq!(parent.id, alice_branch.id);
|
|
191
|
+
|
|
192
|
+
println!("✓ Step 9: Verified state persistence and recovery");
|
|
193
|
+
|
|
194
|
+
// Summary
|
|
195
|
+
println!("\n✅ END-TO-END STATE MODEL TEST PASSED");
|
|
196
|
+
println!(" - Created and branched states");
|
|
197
|
+
println!(" - Verified causal topology");
|
|
198
|
+
println!(" - Computed semantic diffs");
|
|
199
|
+
println!(" - Classified conflicts");
|
|
200
|
+
println!(" - Applied explicit reconciliation");
|
|
201
|
+
println!(" - Verified persistence and recovery");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#[test]
|
|
205
|
+
fn test_convergent_conflict_detection() {
|
|
206
|
+
// Test where both branches make the same change (convergent)
|
|
207
|
+
let store = StateStore::new_volatile();
|
|
208
|
+
let initial = store
|
|
209
|
+
.create(
|
|
210
|
+
r#"{"status":"initial"}"#.to_string(),
|
|
211
|
+
"auth".to_string(),
|
|
212
|
+
)
|
|
213
|
+
.unwrap();
|
|
214
|
+
|
|
215
|
+
let both_updated = r#"{"status":"updated"}"#.to_string();
|
|
216
|
+
|
|
217
|
+
let left = store
|
|
218
|
+
.commit(both_updated.clone(), &initial, "auth".to_string())
|
|
219
|
+
.unwrap();
|
|
220
|
+
|
|
221
|
+
let right = StateRevision::child(both_updated, &initial, "auth".to_string());
|
|
222
|
+
|
|
223
|
+
let classification = ConflictClassification::classify(&initial, &left, &right);
|
|
224
|
+
assert_eq!(classification.overall, ConflictClass::Convergent);
|
|
225
|
+
|
|
226
|
+
println!("✓ Convergent conflict correctly detected");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
#[test]
|
|
230
|
+
fn test_actual_conflict_detection() {
|
|
231
|
+
// Test where both branches make conflicting changes (conflict)
|
|
232
|
+
let store = StateStore::new_volatile();
|
|
233
|
+
let initial = store
|
|
234
|
+
.create(
|
|
235
|
+
r#"{"value":"initial"}"#.to_string(),
|
|
236
|
+
"auth".to_string(),
|
|
237
|
+
)
|
|
238
|
+
.unwrap();
|
|
239
|
+
|
|
240
|
+
let left = store
|
|
241
|
+
.commit(
|
|
242
|
+
r#"{"value":"left-version"}"#.to_string(),
|
|
243
|
+
&initial,
|
|
244
|
+
"auth".to_string(),
|
|
245
|
+
)
|
|
246
|
+
.unwrap();
|
|
247
|
+
|
|
248
|
+
let right = StateRevision::child(
|
|
249
|
+
r#"{"value":"right-version"}"#.to_string(),
|
|
250
|
+
&initial,
|
|
251
|
+
"auth".to_string(),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
let classification = ConflictClassification::classify(&initial, &left, &right);
|
|
255
|
+
assert_eq!(classification.overall, ConflictClass::Conflict);
|
|
256
|
+
|
|
257
|
+
println!("✓ Actual conflict correctly detected");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
#[test]
|
|
261
|
+
fn test_unrelated_histories() {
|
|
262
|
+
// Test that unrelated branches are detected as such
|
|
263
|
+
let store1 = StateStore::new_volatile();
|
|
264
|
+
let state1 = store1
|
|
265
|
+
.create(r#"{"id":1}"#.to_string(), "auth1".to_string())
|
|
266
|
+
.unwrap();
|
|
267
|
+
|
|
268
|
+
let store2 = StateStore::new_volatile();
|
|
269
|
+
let state2 = store2
|
|
270
|
+
.create(r#"{"id":2}"#.to_string(), "auth2".to_string())
|
|
271
|
+
.unwrap();
|
|
272
|
+
|
|
273
|
+
let mut topology = StateTopology::new();
|
|
274
|
+
topology.add_revision(state1.clone());
|
|
275
|
+
topology.add_revision(state2.clone());
|
|
276
|
+
|
|
277
|
+
assert_eq!(
|
|
278
|
+
topology.relationship(&state1.id, &state2.id),
|
|
279
|
+
Relationship::Unrelated
|
|
280
|
+
);
|
|
281
|
+
assert!(topology.common_ancestor(&state1.id, &state2.id).is_none());
|
|
282
|
+
|
|
283
|
+
println!("✓ Unrelated histories correctly identified");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#[test]
|
|
287
|
+
fn test_representation_sensitivity_in_diff() {
|
|
288
|
+
// Verify that representation-sensitive changes are tracked
|
|
289
|
+
let old = json!({"count": 1});
|
|
290
|
+
let new_int = json!({"count": 1}); // Same numeric value
|
|
291
|
+
let new_string = json!({"count": "1"}); // Different representation
|
|
292
|
+
|
|
293
|
+
let diff1 = SemanticDiff::compute(&old, &new_int);
|
|
294
|
+
let diff2 = SemanticDiff::compute(&old, &new_string);
|
|
295
|
+
|
|
296
|
+
assert_eq!(diff1.changes.len(), 0, "No change for same representation");
|
|
297
|
+
assert_eq!(diff2.changes.len(), 1, "Change detected for different representation");
|
|
298
|
+
|
|
299
|
+
println!("✓ Representation sensitivity verified in diffs");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#[test]
|
|
303
|
+
fn test_branching_does_not_advance_current() {
|
|
304
|
+
// Verify that creating a branch doesn't automatically advance current pointer
|
|
305
|
+
let store = StateStore::new_volatile();
|
|
306
|
+
let state1 = store
|
|
307
|
+
.create(r#"{"v":1}"#.to_string(), "auth".to_string())
|
|
308
|
+
.unwrap();
|
|
309
|
+
|
|
310
|
+
let current_after_create = store.current().unwrap();
|
|
311
|
+
assert_eq!(current_after_create.id, state1.id);
|
|
312
|
+
|
|
313
|
+
// Branch from state1
|
|
314
|
+
let state2 = store
|
|
315
|
+
.commit(r#"{"v":2}"#.to_string(), &state1, "auth".to_string())
|
|
316
|
+
.unwrap();
|
|
317
|
+
|
|
318
|
+
store
|
|
319
|
+
.create_branch("test-branch".to_string(), state2.id.clone())
|
|
320
|
+
.unwrap();
|
|
321
|
+
|
|
322
|
+
// Current should still be state2 (because we committed it)
|
|
323
|
+
let current_after_branch = store.current().unwrap();
|
|
324
|
+
assert_eq!(current_after_branch.id, state2.id);
|
|
325
|
+
|
|
326
|
+
println!("✓ Branch creation verified (doesn't implicitly advance current)");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
#[test]
|
|
330
|
+
fn test_no_automatic_merge() {
|
|
331
|
+
// Verify that reconciliation requires explicit plan
|
|
332
|
+
let store = StateStore::new_volatile();
|
|
333
|
+
let initial = store
|
|
334
|
+
.create(r#"{"a":1,"b":2}"#.to_string(), "auth".to_string())
|
|
335
|
+
.unwrap();
|
|
336
|
+
|
|
337
|
+
let left = store
|
|
338
|
+
.commit(r#"{"a":10,"b":2}"#.to_string(), &initial, "auth".to_string())
|
|
339
|
+
.unwrap();
|
|
340
|
+
|
|
341
|
+
let right = StateRevision::child(
|
|
342
|
+
r#"{"a":1,"b":20}"#.to_string(),
|
|
343
|
+
&initial,
|
|
344
|
+
"auth".to_string(),
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
// Classify but don't automatically merge
|
|
348
|
+
let classification = ConflictClassification::classify(&initial, &left, &right);
|
|
349
|
+
|
|
350
|
+
// Caller must supply explicit plan
|
|
351
|
+
let plan = ReconciliationPlan::new(
|
|
352
|
+
left.id.clone(),
|
|
353
|
+
right.id.clone(),
|
|
354
|
+
initial.id.clone(),
|
|
355
|
+
true,
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
assert!(plan.validate());
|
|
359
|
+
assert!(
|
|
360
|
+
plan.path_overrides.is_empty(),
|
|
361
|
+
"Plan starts empty; caller must supply overrides"
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
println!("✓ No automatic merge; explicit plan required");
|
|
365
|
+
}
|
|
366
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
//! Comprehensive tests for FeltDB State Model persistence
|
|
2
|
+
//!
|
|
3
|
+
//! This test suite verifies that:
|
|
4
|
+
//! 1. State revisions persist to disk through FeltDB
|
|
5
|
+
//! 2. Current pointer persists correctly
|
|
6
|
+
//! 3. Branches persist and recover
|
|
7
|
+
//! 4. Restart recovery works (close database, reopen and recover state)
|
|
8
|
+
//! 5. Deterministic StateIds survive restart
|
|
9
|
+
//! 6. Volatile stores remain unchanged
|
|
10
|
+
|
|
11
|
+
#[cfg(test)]
|
|
12
|
+
mod state_persistence_tests {
|
|
13
|
+
use feltdb::state_model::StateStore;
|
|
14
|
+
use std::sync::Arc;
|
|
15
|
+
use tempfile::TempDir;
|
|
16
|
+
|
|
17
|
+
#[test]
|
|
18
|
+
fn test_persistent_store_saves_revisions_to_disk() {
|
|
19
|
+
let temp_dir = TempDir::new().unwrap();
|
|
20
|
+
let temp_path = temp_dir.path().join("test.log");
|
|
21
|
+
|
|
22
|
+
// Create initial state with FeltDB persistence
|
|
23
|
+
let db1 = feltdb::FeltDb::open(&temp_path).expect("open db");
|
|
24
|
+
let store1 = StateStore::with_feltdb(Arc::new(db1)).expect("create store");
|
|
25
|
+
let rev1 = store1.create(r#"{"v":1}"#.to_string(), "auth".to_string()).expect("create");
|
|
26
|
+
let rev2 = store1.commit(r#"{"v":2}"#.to_string(), &rev1, "auth".to_string()).expect("commit");
|
|
27
|
+
|
|
28
|
+
let id1 = rev1.id.clone();
|
|
29
|
+
let id2 = rev2.id.clone();
|
|
30
|
+
let id2_hex = id2.as_hex().to_string();
|
|
31
|
+
|
|
32
|
+
// Verify both revisions exist
|
|
33
|
+
assert!(store1.exists(&id1), "rev1 should exist");
|
|
34
|
+
assert!(store1.exists(&id2), "rev2 should exist");
|
|
35
|
+
assert_eq!(store1.current().map(|r| r.id.as_hex().to_string()), Some(id2_hex.clone()));
|
|
36
|
+
|
|
37
|
+
// Drop store and database (simulate restart)
|
|
38
|
+
drop(store1);
|
|
39
|
+
|
|
40
|
+
// New process: recover from disk
|
|
41
|
+
let db2 = feltdb::FeltDb::open(&temp_path).expect("open db again");
|
|
42
|
+
let store2 = StateStore::with_feltdb(Arc::new(db2)).expect("recovery setup");
|
|
43
|
+
|
|
44
|
+
// Verify all revisions still exist
|
|
45
|
+
assert!(store2.exists(&id1), "rev1 should survive restart");
|
|
46
|
+
assert!(store2.exists(&id2), "rev2 should survive restart");
|
|
47
|
+
assert_eq!(store2.current().map(|r| r.id.as_hex().to_string()), Some(id2_hex), "current should be restored");
|
|
48
|
+
|
|
49
|
+
// Verify content is correct
|
|
50
|
+
let recovered_rev2 = store2.get(&id2).expect("get rev2");
|
|
51
|
+
assert_eq!(recovered_rev2.content, r#"{"v":2}"#);
|
|
52
|
+
assert_eq!(recovered_rev2.authority, "auth");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#[test]
|
|
56
|
+
fn test_persistent_store_preserves_state_id_determinism() {
|
|
57
|
+
let temp_dir = TempDir::new().unwrap();
|
|
58
|
+
let temp_path = temp_dir.path().join("test.log");
|
|
59
|
+
|
|
60
|
+
let db1 = feltdb::FeltDb::open(&temp_path).expect("open db");
|
|
61
|
+
let store1 = StateStore::with_feltdb(Arc::new(db1)).expect("create store");
|
|
62
|
+
let rev1 = store1.create(r#"{"fixed":"content"}"#.to_string(), "auth".to_string()).expect("create");
|
|
63
|
+
let id1 = rev1.id.clone();
|
|
64
|
+
drop(store1);
|
|
65
|
+
|
|
66
|
+
// Recover and recreate the same content
|
|
67
|
+
let db2 = feltdb::FeltDb::open(&temp_path).expect("open db again");
|
|
68
|
+
let store2 = StateStore::with_feltdb(Arc::new(db2)).expect("recovery");
|
|
69
|
+
let recovered = store2.get(&id1).expect("get");
|
|
70
|
+
|
|
71
|
+
// Content hash must be the same for identical content
|
|
72
|
+
assert_eq!(recovered.id.as_hex(), id1.as_hex(), "StateId should be deterministic");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#[test]
|
|
76
|
+
fn test_persistent_store_handles_branch_persistence() {
|
|
77
|
+
let temp_dir = TempDir::new().unwrap();
|
|
78
|
+
let temp_path = temp_dir.path().join("test.log");
|
|
79
|
+
|
|
80
|
+
// Create store and initial state
|
|
81
|
+
let db1 = feltdb::FeltDb::open(&temp_path).expect("open db");
|
|
82
|
+
let store1 = StateStore::with_feltdb(Arc::new(db1)).expect("create store");
|
|
83
|
+
let rev1 = store1.create(r#"{"v":1}"#.to_string(), "auth".to_string()).expect("create");
|
|
84
|
+
|
|
85
|
+
let id1 = rev1.id.clone();
|
|
86
|
+
let id1_hex = id1.as_hex().to_string();
|
|
87
|
+
|
|
88
|
+
// Create a branch
|
|
89
|
+
store1.create_branch("main".to_string(), id1.clone()).expect("branch");
|
|
90
|
+
|
|
91
|
+
// Verify branch exists
|
|
92
|
+
assert_eq!(store1.branch_head("main").map(|id| id.as_hex().to_string()), Some(id1_hex.clone()));
|
|
93
|
+
drop(store1);
|
|
94
|
+
|
|
95
|
+
// Recover and check branch
|
|
96
|
+
let db2 = feltdb::FeltDb::open(&temp_path).expect("open db again");
|
|
97
|
+
let store2 = StateStore::with_feltdb(Arc::new(db2)).expect("recovery");
|
|
98
|
+
assert_eq!(store2.branch_head("main").map(|id| id.as_hex().to_string()), Some(id1_hex), "branch should survive restart");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#[test]
|
|
102
|
+
fn test_volatile_store_does_not_persist() {
|
|
103
|
+
let temp_dir = TempDir::new().unwrap();
|
|
104
|
+
let temp_path = temp_dir.path().join("test.log");
|
|
105
|
+
|
|
106
|
+
// Create volatile store
|
|
107
|
+
let store1 = StateStore::new_volatile();
|
|
108
|
+
let rev1 = store1.create(r#"{"v":1}"#.to_string(), "auth".to_string()).expect("create");
|
|
109
|
+
drop(store1);
|
|
110
|
+
|
|
111
|
+
// Try to recover from disk (should be empty)
|
|
112
|
+
let db2 = feltdb::FeltDb::open(&temp_path).expect("open db");
|
|
113
|
+
let store2 = StateStore::with_feltdb(Arc::new(db2)).expect("create store");
|
|
114
|
+
assert!(!store2.exists(&rev1.id), "volatile store should not persist");
|
|
115
|
+
assert!(store2.current().is_none(), "should have no current state");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#[test]
|
|
119
|
+
fn test_persistent_store_tracks_ancestry_across_restart() {
|
|
120
|
+
let temp_dir = TempDir::new().unwrap();
|
|
121
|
+
let temp_path = temp_dir.path().join("test.log");
|
|
122
|
+
|
|
123
|
+
// Create chain of revisions
|
|
124
|
+
let db1 = feltdb::FeltDb::open(&temp_path).expect("open db");
|
|
125
|
+
let store1 = StateStore::with_feltdb(Arc::new(db1)).expect("create store");
|
|
126
|
+
let rev1 = store1.create(r#"{"v":1}"#.to_string(), "auth".to_string()).expect("create");
|
|
127
|
+
let rev2 = store1.commit(r#"{"v":2}"#.to_string(), &rev1, "auth".to_string()).expect("commit");
|
|
128
|
+
let rev3 = store1.commit(r#"{"v":3}"#.to_string(), &rev2, "auth".to_string()).expect("commit");
|
|
129
|
+
|
|
130
|
+
let id1 = rev1.id.clone();
|
|
131
|
+
let id2 = rev2.id.clone();
|
|
132
|
+
let id3 = rev3.id.clone();
|
|
133
|
+
drop(store1);
|
|
134
|
+
|
|
135
|
+
// Recover and verify parent pointers
|
|
136
|
+
let db2 = feltdb::FeltDb::open(&temp_path).expect("open db again");
|
|
137
|
+
let store2 = StateStore::with_feltdb(Arc::new(db2)).expect("recovery");
|
|
138
|
+
|
|
139
|
+
let recovered_rev3 = store2.get(&id3).expect("get rev3");
|
|
140
|
+
assert_eq!(recovered_rev3.parent_id.as_ref().map(|id| id.as_hex()), Some(id2.as_hex()), "rev3 parent should be rev2");
|
|
141
|
+
|
|
142
|
+
let recovered_rev2 = store2.get(&id2).expect("get rev2");
|
|
143
|
+
assert_eq!(recovered_rev2.parent_id.as_ref().map(|id| id.as_hex()), Some(id1.as_hex()), "rev2 parent should be rev1");
|
|
144
|
+
|
|
145
|
+
let recovered_rev1 = store2.get(&id1).expect("get rev1");
|
|
146
|
+
assert!(recovered_rev1.parent_id.is_none(), "rev1 should have no parent");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#[test]
|
|
150
|
+
fn test_persistent_store_multiple_commits_in_sequence() {
|
|
151
|
+
let temp_dir = TempDir::new().unwrap();
|
|
152
|
+
let temp_path = temp_dir.path().join("test.log");
|
|
153
|
+
|
|
154
|
+
let db = feltdb::FeltDb::open(&temp_path).expect("open db");
|
|
155
|
+
let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
|
|
156
|
+
|
|
157
|
+
// Commit sequence
|
|
158
|
+
let rev1 = store.create(r#"{"step":1}"#.to_string(), "app".to_string()).expect("1");
|
|
159
|
+
let rev2 = store.commit(r#"{"step":2}"#.to_string(), &rev1, "app".to_string()).expect("2");
|
|
160
|
+
let rev3 = store.commit(r#"{"step":3}"#.to_string(), &rev2, "app".to_string()).expect("3");
|
|
161
|
+
let rev4 = store.commit(r#"{"step":4}"#.to_string(), &rev3, "app".to_string()).expect("4");
|
|
162
|
+
let rev5 = store.commit(r#"{"step":5}"#.to_string(), &rev4, "app".to_string()).expect("5");
|
|
163
|
+
|
|
164
|
+
let id1 = rev1.id.clone();
|
|
165
|
+
let id2 = rev2.id.clone();
|
|
166
|
+
let id3 = rev3.id.clone();
|
|
167
|
+
let id4 = rev4.id.clone();
|
|
168
|
+
let id5 = rev5.id.clone();
|
|
169
|
+
|
|
170
|
+
// All should exist
|
|
171
|
+
assert!(store.exists(&id1));
|
|
172
|
+
assert!(store.exists(&id2));
|
|
173
|
+
assert!(store.exists(&id3));
|
|
174
|
+
assert!(store.exists(&id4));
|
|
175
|
+
assert!(store.exists(&id5));
|
|
176
|
+
let id5_hex = id5.as_hex().to_string();
|
|
177
|
+
assert_eq!(store.current().map(|r| r.id.as_hex().to_string()), Some(id5_hex));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#[test]
|
|
181
|
+
fn test_persistence_with_corrupted_content() {
|
|
182
|
+
let temp_dir = TempDir::new().unwrap();
|
|
183
|
+
let temp_path = temp_dir.path().join("test.log");
|
|
184
|
+
|
|
185
|
+
// Create and persist
|
|
186
|
+
let db1 = feltdb::FeltDb::open(&temp_path).expect("open db");
|
|
187
|
+
let store1 = StateStore::with_feltdb(Arc::new(db1)).expect("create store");
|
|
188
|
+
let rev1 = store1.create(
|
|
189
|
+
r#"{"complex":{"nested":{"data":[1,2,3],"unicode":"αβγ"}}}"#.to_string(),
|
|
190
|
+
"auth".to_string()
|
|
191
|
+
).expect("create");
|
|
192
|
+
let id1 = rev1.id.clone();
|
|
193
|
+
drop(store1);
|
|
194
|
+
|
|
195
|
+
// Recover
|
|
196
|
+
let db2 = feltdb::FeltDb::open(&temp_path).expect("open db again");
|
|
197
|
+
let store2 = StateStore::with_feltdb(Arc::new(db2)).expect("recovery");
|
|
198
|
+
let recovered = store2.get(&id1).expect("get");
|
|
199
|
+
assert_eq!(recovered.content, r#"{"complex":{"nested":{"data":[1,2,3],"unicode":"αβγ"}}}"#);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#[test]
|
|
203
|
+
fn test_concurrent_access_to_persistent_store() {
|
|
204
|
+
let temp_dir = TempDir::new().unwrap();
|
|
205
|
+
let temp_path = temp_dir.path().join("test.log");
|
|
206
|
+
|
|
207
|
+
let db = feltdb::FeltDb::open(&temp_path).expect("open db");
|
|
208
|
+
let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
|
|
209
|
+
let rev1 = store.create(r#"{"v":1}"#.to_string(), "auth".to_string()).expect("create");
|
|
210
|
+
let id1 = rev1.id.clone();
|
|
211
|
+
|
|
212
|
+
// Clone the store for concurrent access
|
|
213
|
+
let store_clone1 = store.clone();
|
|
214
|
+
let store_clone2 = store.clone();
|
|
215
|
+
|
|
216
|
+
let id1_hex = id1.as_hex().to_string();
|
|
217
|
+
|
|
218
|
+
// Both can read
|
|
219
|
+
assert_eq!(store_clone1.current().map(|r| r.id.as_hex().to_string()), Some(id1_hex.clone()));
|
|
220
|
+
assert_eq!(store_clone2.current().map(|r| r.id.as_hex().to_string()), Some(id1_hex));
|
|
221
|
+
|
|
222
|
+
// Commit from one
|
|
223
|
+
let rev2 = store_clone1.commit(r#"{"v":2}"#.to_string(), &rev1, "auth".to_string()).expect("commit");
|
|
224
|
+
let id2 = rev2.id.clone();
|
|
225
|
+
|
|
226
|
+
// Both should see the new revision
|
|
227
|
+
assert!(store.exists(&id2));
|
|
228
|
+
assert!(store_clone2.exists(&id2));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
#[test]
|
|
232
|
+
fn test_persistent_store_recovery_after_multiple_crashes() {
|
|
233
|
+
let temp_dir = TempDir::new().unwrap();
|
|
234
|
+
let temp_path = temp_dir.path().join("test.log");
|
|
235
|
+
|
|
236
|
+
// Cycle 1: create initial state
|
|
237
|
+
{
|
|
238
|
+
let db = feltdb::FeltDb::open(&temp_path).expect("open db");
|
|
239
|
+
let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
|
|
240
|
+
let rev1 = store.create(r#"{"cycle":1}"#.to_string(), "auth".to_string()).expect("create1");
|
|
241
|
+
let _rev2 = store.commit(r#"{"cycle":1.5}"#.to_string(), &rev1, "auth".to_string()).expect("commit1");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Cycle 2: recover and add more
|
|
245
|
+
{
|
|
246
|
+
let db = feltdb::FeltDb::open(&temp_path).expect("open db");
|
|
247
|
+
let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
|
|
248
|
+
let rev2 = store.current().expect("current");
|
|
249
|
+
let _rev3 = store.commit(r#"{"cycle":2}"#.to_string(), &rev2, "auth".to_string()).expect("commit2");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Cycle 3: verify all history
|
|
253
|
+
{
|
|
254
|
+
let db = feltdb::FeltDb::open(&temp_path).expect("open db");
|
|
255
|
+
let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
|
|
256
|
+
assert!(store.current().is_some(), "should have current state after 3 cycles");
|
|
257
|
+
|
|
258
|
+
// Walk the chain backwards - count how many nodes we can reach
|
|
259
|
+
let mut rev = store.current().expect("current");
|
|
260
|
+
let mut count = 1; // Start with 1 for the current node
|
|
261
|
+
while let Some(parent_id) = &rev.parent_id {
|
|
262
|
+
rev = store.get(parent_id).expect("get parent");
|
|
263
|
+
count += 1;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
assert!(count >= 3, "should have chain of at least 3 revisions, got {}", count);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
use std::path::PathBuf;
|
|
2
2
|
use std::{
|
|
3
|
+
collections::HashMap,
|
|
3
4
|
sync::{atomic::AtomicU64, Arc},
|
|
4
5
|
time::Instant,
|
|
5
6
|
};
|
|
7
|
+
use serde_json::Value;
|
|
8
|
+
|
|
9
|
+
#[derive(Clone)]
|
|
10
|
+
pub struct BoundedQueryCursor {
|
|
11
|
+
pub query_hash: String,
|
|
12
|
+
pub principal_key_id: String,
|
|
13
|
+
pub namespace: String,
|
|
14
|
+
pub records: Arc<Vec<Value>>,
|
|
15
|
+
pub position: usize,
|
|
16
|
+
pub created_at: u64,
|
|
17
|
+
}
|
|
6
18
|
|
|
7
19
|
use feltdb::{
|
|
8
20
|
FeltDb,
|
|
@@ -74,4 +86,5 @@ pub struct AppState {
|
|
|
74
86
|
pub workloads: Arc<std::sync::Mutex<WorkloadStore>>,
|
|
75
87
|
pub mesh: Arc<std::sync::Mutex<WorkerMeshStore>>,
|
|
76
88
|
pub readiness_probe: Arc<PathBuf>,
|
|
89
|
+
pub bounded_query_cursors: Arc<std::sync::Mutex<HashMap<String, BoundedQueryCursor>>>,
|
|
77
90
|
}
|