@feltdb/core 0.4.11 → 0.4.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands.js +5 -2
- package/dist/cli/index.js +1 -1
- package/dist/create/cli.js +16 -1
- package/dist/create/create.js +30 -16
- package/dist/create/docker-compose-generator.js +48 -9
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +2238 -0
- package/dist/create/server-source/Cargo.toml +7 -0
- package/dist/create/server-source/crates/feltdb/Cargo.lock +175 -0
- package/dist/create/server-source/crates/feltdb/Cargo.toml +79 -0
- package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +370 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +589 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +259 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +446 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +272 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +378 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +414 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +486 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +273 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +587 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +513 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +395 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +469 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +409 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +627 -0
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +383 -0
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +298 -0
- package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +1698 -0
- package/dist/create/server-source/crates/feltdb/src/acquisition.rs +286 -0
- package/dist/create/server-source/crates/feltdb/src/admission.rs +192 -0
- package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +477 -0
- package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +566 -0
- package/dist/create/server-source/crates/feltdb/src/analytics.rs +475 -0
- package/dist/create/server-source/crates/feltdb/src/application.rs +2244 -0
- package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +1070 -0
- package/dist/create/server-source/crates/feltdb/src/authorization.rs +1030 -0
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +266 -0
- package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +8 -0
- package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +418 -0
- package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +482 -0
- package/dist/create/server-source/crates/feltdb/src/capability.rs +903 -0
- package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +261 -0
- package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +78 -0
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +2214 -0
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +194 -0
- package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +427 -0
- package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +453 -0
- package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +465 -0
- package/dist/create/server-source/crates/feltdb/src/convergence.rs +618 -0
- package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +418 -0
- package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +380 -0
- package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +362 -0
- package/dist/create/server-source/crates/feltdb/src/cron.rs +294 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +474 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +1003 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +536 -0
- package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +364 -0
- package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +235 -0
- package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +207 -0
- package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +316 -0
- package/dist/create/server-source/crates/feltdb/src/execution.rs +426 -0
- package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +219 -0
- package/dist/create/server-source/crates/feltdb/src/indexing.rs +779 -0
- package/dist/create/server-source/crates/feltdb/src/lib.rs +2838 -0
- package/dist/create/server-source/crates/feltdb/src/materialization.rs +184 -0
- package/dist/create/server-source/crates/feltdb/src/metrics.rs +267 -0
- package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +311 -0
- package/dist/create/server-source/crates/feltdb/src/observability.rs +366 -0
- package/dist/create/server-source/crates/feltdb/src/operation.rs +251 -0
- package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +438 -0
- package/dist/create/server-source/crates/feltdb/src/operation_log.rs +344 -0
- package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +477 -0
- package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +166 -0
- package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +261 -0
- package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +560 -0
- package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +3226 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +201 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +263 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +484 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +216 -0
- package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +281 -0
- package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +323 -0
- package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +573 -0
- package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +404 -0
- package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +418 -0
- package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +381 -0
- package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +523 -0
- package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +334 -0
- package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +306 -0
- package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +355 -0
- package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +395 -0
- package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +346 -0
- package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +430 -0
- package/dist/create/server-source/crates/feltdb/src/production_api.rs +400 -0
- package/dist/create/server-source/crates/feltdb/src/provenance.rs +207 -0
- package/dist/create/server-source/crates/feltdb/src/query_performance.rs +217 -0
- package/dist/create/server-source/crates/feltdb/src/references.rs +404 -0
- package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +401 -0
- package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +160 -0
- package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +132 -0
- package/dist/create/server-source/crates/feltdb/src/routing.rs +409 -0
- package/dist/create/server-source/crates/feltdb/src/sharding.rs +523 -0
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +3118 -0
- package/dist/create/server-source/crates/feltdb/src/state_hash.rs +291 -0
- package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +376 -0
- package/dist/create/server-source/crates/feltdb/src/storage.rs +267 -0
- package/dist/create/server-source/crates/feltdb/src/submission.rs +477 -0
- package/dist/create/server-source/crates/feltdb/src/sync.rs +483 -0
- package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +822 -0
- package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +366 -0
- package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +473 -0
- package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +949 -0
- package/dist/create/server-source/crates/feltdb/src/transactions.rs +646 -0
- package/dist/create/server-source/crates/feltdb/src/trigger.rs +390 -0
- package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +928 -0
- package/dist/create/server-source/crates/feltdb/src/workflow.rs +713 -0
- package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +510 -0
- package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +354 -0
- package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +594 -0
- package/dist/create/server-source/crates/feltdb/src/workload.rs +1310 -0
- package/dist/create/server-source/crates/feltdb-server/Cargo.toml +23 -0
- package/dist/create/server-source/crates/feltdb-server/README.md +79 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +73 -0
- package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +425 -0
- package/dist/create/server-source/crates/feltdb-server/src/artifacts.rs +449 -0
- package/dist/create/server-source/crates/feltdb-server/src/audit.rs +59 -0
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +308 -0
- package/dist/create/server-source/crates/feltdb-server/src/authorization.rs +228 -0
- package/dist/create/server-source/crates/feltdb-server/src/backup.rs +416 -0
- package/dist/create/server-source/crates/feltdb-server/src/causal.rs +218 -0
- package/dist/create/server-source/crates/feltdb-server/src/certification.rs +223 -0
- package/dist/create/server-source/crates/feltdb-server/src/clock.rs +132 -0
- package/dist/create/server-source/crates/feltdb-server/src/cluster.rs +165 -0
- package/dist/create/server-source/crates/feltdb-server/src/connections.rs +1044 -0
- package/dist/create/server-source/crates/feltdb-server/src/content.rs +111 -0
- package/dist/create/server-source/crates/feltdb-server/src/identity.rs +250 -0
- package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +78 -0
- package/dist/create/server-source/crates/feltdb-server/src/key_provider.rs +247 -0
- package/dist/create/server-source/crates/feltdb-server/src/leases.rs +123 -0
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +25 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +8715 -0
- package/dist/create/server-source/crates/feltdb-server/src/metrics.rs +97 -0
- package/dist/create/server-source/crates/feltdb-server/src/portable_bundle.rs +350 -0
- package/dist/create/server-source/crates/feltdb-server/src/principals.rs +510 -0
- package/dist/create/server-source/crates/feltdb-server/src/providers.rs +269 -0
- package/dist/create/server-source/crates/feltdb-server/src/releases.rs +2563 -0
- package/dist/create/server-source/crates/feltdb-server/src/sessions.rs +156 -0
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +1097 -0
- package/dist/create/server-source/crates/feltdb-server/src/versions.rs +264 -0
- package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +31 -0
- package/dist/create/server-source/crates/feltdb-wasm/src/lib.rs +1086 -0
- package/dist/create/server-source/crates/feltdb-wasm/test.db +0 -0
- package/dist/flowspec.d.ts +2 -0
- package/dist/flowspec.d.ts.map +1 -1
- package/dist/flowspec.js +28 -4
- package/dist/state-contract.d.ts +7 -1
- package/dist/state-contract.d.ts.map +1 -1
- package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
- package/dist/studio/components/index.js +1 -1
- package/dist/studio/{components-q43NSTTH.js → components-bHTARBin.js} +160 -89
- package/dist/studio/index.js +31 -28
- package/dist/studio-app/assets/{index-DZpBoVMp.js → index-BqKquLuU.js} +8 -8
- package/dist/studio-app/index.html +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,2838 @@
|
|
|
1
|
+
#[cfg(test)]
|
|
2
|
+
mod acceptance_tests;
|
|
3
|
+
#[cfg(test)]
|
|
4
|
+
mod admission_contract_tests;
|
|
5
|
+
mod acquisition;
|
|
6
|
+
pub mod admission;
|
|
7
|
+
pub mod application;
|
|
8
|
+
pub mod application_runtime;
|
|
9
|
+
pub mod authorization;
|
|
10
|
+
pub mod capabilities;
|
|
11
|
+
mod capability;
|
|
12
|
+
mod content_distribution;
|
|
13
|
+
pub mod convergence;
|
|
14
|
+
mod cron;
|
|
15
|
+
#[cfg(test)]
|
|
16
|
+
mod distributed_tests;
|
|
17
|
+
mod execution;
|
|
18
|
+
pub mod analytics;
|
|
19
|
+
pub mod distributed_indexing;
|
|
20
|
+
pub mod indexing;
|
|
21
|
+
pub mod sharding;
|
|
22
|
+
pub mod transactions;
|
|
23
|
+
pub mod state_hash;
|
|
24
|
+
pub mod crash_injection;
|
|
25
|
+
pub mod concurrency_fuzzing;
|
|
26
|
+
pub mod replay_fuzzing;
|
|
27
|
+
pub mod permutation_scheduler;
|
|
28
|
+
pub mod multi_node_convergence;
|
|
29
|
+
pub mod distributed_transactions;
|
|
30
|
+
pub mod durable_operation_log;
|
|
31
|
+
pub mod durable_dedup_set;
|
|
32
|
+
pub mod replication_protocol;
|
|
33
|
+
pub mod in_process_transport;
|
|
34
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
35
|
+
pub mod tcp_transport;
|
|
36
|
+
pub mod operation_algebra;
|
|
37
|
+
pub mod crash_atomic_boundary;
|
|
38
|
+
pub mod causal_dependency_barrier;
|
|
39
|
+
pub mod partition_reconciliation;
|
|
40
|
+
pub mod consistency_contract;
|
|
41
|
+
pub mod persistence_reality;
|
|
42
|
+
pub mod adversarial_transport;
|
|
43
|
+
pub mod replication_manager;
|
|
44
|
+
pub mod metrics;
|
|
45
|
+
pub mod query_performance;
|
|
46
|
+
pub mod durable_sync;
|
|
47
|
+
pub mod production_api;
|
|
48
|
+
pub mod observability;
|
|
49
|
+
pub mod durability_guarantees;
|
|
50
|
+
#[cfg(test)]
|
|
51
|
+
mod phase1b_acceptance;
|
|
52
|
+
#[cfg(test)]
|
|
53
|
+
mod phase1c1_acceptance;
|
|
54
|
+
#[cfg(test)]
|
|
55
|
+
mod phase1c2_acceptance;
|
|
56
|
+
#[cfg(test)]
|
|
57
|
+
mod phase1c_atomicity_proof;
|
|
58
|
+
#[cfg(test)]
|
|
59
|
+
mod phase1c3_acceptance;
|
|
60
|
+
#[cfg(test)]
|
|
61
|
+
mod phase_1c_three_node;
|
|
62
|
+
#[cfg(test)]
|
|
63
|
+
mod phase_1c_real_tcp;
|
|
64
|
+
#[cfg(test)]
|
|
65
|
+
mod phase_2a_failures;
|
|
66
|
+
#[cfg(test)]
|
|
67
|
+
mod phase_2b_network;
|
|
68
|
+
#[cfg(test)]
|
|
69
|
+
mod phase_2c_cascading;
|
|
70
|
+
#[cfg(test)]
|
|
71
|
+
mod phase_3_durability;
|
|
72
|
+
#[cfg(test)]
|
|
73
|
+
mod phase_4_baseline;
|
|
74
|
+
#[cfg(test)]
|
|
75
|
+
mod phase_5_soak;
|
|
76
|
+
#[cfg(test)]
|
|
77
|
+
mod phase5_integration;
|
|
78
|
+
#[cfg(test)]
|
|
79
|
+
mod phase5_scenarios;
|
|
80
|
+
#[cfg(test)]
|
|
81
|
+
mod phase6_persistence;
|
|
82
|
+
#[cfg(test)]
|
|
83
|
+
mod phase6_adversarial_scenarios;
|
|
84
|
+
#[cfg(test)]
|
|
85
|
+
mod phase6_convergence_validator;
|
|
86
|
+
#[cfg(test)]
|
|
87
|
+
mod transaction_invariants;
|
|
88
|
+
#[cfg(test)]
|
|
89
|
+
mod crash_recovery_tests;
|
|
90
|
+
#[cfg(test)]
|
|
91
|
+
mod causal_dependency_barrier_phase_7_1;
|
|
92
|
+
mod cardinality_diagnostics;
|
|
93
|
+
pub mod cardinality_endpoint;
|
|
94
|
+
mod materialization;
|
|
95
|
+
mod operation;
|
|
96
|
+
pub mod operation_log;
|
|
97
|
+
pub mod state_transition_store;
|
|
98
|
+
pub mod submission;
|
|
99
|
+
pub mod transaction_api;
|
|
100
|
+
mod peer_registry;
|
|
101
|
+
mod provenance;
|
|
102
|
+
mod references;
|
|
103
|
+
mod routing;
|
|
104
|
+
pub mod state_contract;
|
|
105
|
+
mod storage;
|
|
106
|
+
mod sync;
|
|
107
|
+
pub mod sync_contract;
|
|
108
|
+
mod trigger;
|
|
109
|
+
pub mod worker_mesh;
|
|
110
|
+
mod workflow;
|
|
111
|
+
#[cfg(test)]
|
|
112
|
+
mod workflow_acceptance_tests;
|
|
113
|
+
mod workflow_integration;
|
|
114
|
+
mod workflow_runtime;
|
|
115
|
+
pub mod workload;
|
|
116
|
+
|
|
117
|
+
pub use acquisition::{
|
|
118
|
+
AcquisitionDeduplicator, AcquisitionPolicy, AcquisitionResult, ContentRequest, ContentResponse,
|
|
119
|
+
ContentTransferError,
|
|
120
|
+
};
|
|
121
|
+
pub use capability::{
|
|
122
|
+
CapabilityAccessModel, CapabilityCheckpoint, CapabilityContext, CapabilityExecutionContext,
|
|
123
|
+
CapabilityExecutionResult, CapabilityId, CapabilityLifecycleState, CapabilityMetadata,
|
|
124
|
+
CapabilityOperationResult, CapabilityQuery, CapabilityQueryResult, CapabilityRef,
|
|
125
|
+
CapabilityRegistry, CapabilityRequirements, CapabilityResolver, CapabilityRole,
|
|
126
|
+
DistributedCapabilityAdvertisement, ExecutionRef, FlowCapability, StateRequirement,
|
|
127
|
+
};
|
|
128
|
+
pub use content_distribution::{
|
|
129
|
+
CapabilityLocationRegistry, ContentAcquisitionManager, ContentAddressableStore,
|
|
130
|
+
DistributedReferenceResolver, ProjectionDistribution,
|
|
131
|
+
};
|
|
132
|
+
pub use convergence::{
|
|
133
|
+
CausalHistory, ConflictResolution, ConflictResolutionStrategy, MergeResult, StateVersion,
|
|
134
|
+
VectorClock,
|
|
135
|
+
};
|
|
136
|
+
pub use analytics::{AutoTuner, IndexAnalytics, IndexMetrics, PerformanceReport, QueryTrace, TuningRecommendation};
|
|
137
|
+
pub use cron::{CronSchedule, CronScheduler};
|
|
138
|
+
pub use distributed_indexing::{
|
|
139
|
+
ConsistencyLevel, ConsistencyProtocol, DistributedIndexManager, DistributedSyncStatus,
|
|
140
|
+
IndexOperation, IndexReplicationState, IndexSyncMessage, IndexVectorClock, RemoteIndexMetadata,
|
|
141
|
+
RemoteIndexState, ReplicationStatus, VersionConflict,
|
|
142
|
+
};
|
|
143
|
+
pub use execution::{Execution, ExecutionQueue, ExecutionStatus, RetryPolicy};
|
|
144
|
+
pub use indexing::{IndexConfig, IndexManager, IndexStats, IndexType};
|
|
145
|
+
pub use submission::{SubmissionManager, SubmissionMetrics};
|
|
146
|
+
pub use admission::{SubmissionOutcome, RejectionReason, ProductionRetryPolicy};
|
|
147
|
+
pub use sharding::{
|
|
148
|
+
HotspotAlert, RebalanceOperation, ShardDistributionSummary, ShardId, ShardKey, ShardManager,
|
|
149
|
+
ShardMetrics, ShardRange, ShardingStrategy,
|
|
150
|
+
};
|
|
151
|
+
pub use transactions::{
|
|
152
|
+
CommitId, CommitValidator, ConsistencyContract, OperationCommand,
|
|
153
|
+
OperationId, StateTransition, StateDependency, TransactionExecutor,
|
|
154
|
+
TransitionResult,
|
|
155
|
+
};
|
|
156
|
+
pub use state_hash::{CanonicalState, StateHash};
|
|
157
|
+
pub use permutation_scheduler::{OperationSchedule, PermutationScheduler, ScheduleStrategy};
|
|
158
|
+
pub use multi_node_convergence::{
|
|
159
|
+
ConvergenceAggregation, ConvergenceResult, MultiNodeConvergenceSimulator, NodeExecutionResult,
|
|
160
|
+
};
|
|
161
|
+
pub use distributed_transactions::{
|
|
162
|
+
DistributedTransactionExecutor, EnvelopeId, ReplicaState, ReplicationMessage, TransactionEnvelope,
|
|
163
|
+
};
|
|
164
|
+
pub use operation_algebra::{
|
|
165
|
+
ConflictType, ConvergenceSemantic, OperationAlgebra, OperationAlgebraRegistry,
|
|
166
|
+
};
|
|
167
|
+
pub use crash_atomic_boundary::{
|
|
168
|
+
CrashAtomicExecutor, CrashBoundaryPoint, CrashRecoveryState, InvariantCheckResult,
|
|
169
|
+
RecoveryDecision,
|
|
170
|
+
};
|
|
171
|
+
pub use causal_dependency_barrier::{
|
|
172
|
+
ApplyResult, CausalDependencyBarrier, OperationState, PendingOperation, ReceiveResult,
|
|
173
|
+
StateCounters,
|
|
174
|
+
};
|
|
175
|
+
pub use partition_reconciliation::{
|
|
176
|
+
DeterminismCheckResult, PartitionDeterminismChecker, PartitionTrace, PartitionedReplica,
|
|
177
|
+
ReconciliationResult, ReplicaPartition,
|
|
178
|
+
};
|
|
179
|
+
pub use consistency_contract::{
|
|
180
|
+
CommutativeVerificationResult, ConsensusEnforcementResult, ConsensusRequirementChecker,
|
|
181
|
+
ConsistencyContractEnforcer, DeterministicMergeVerificationResult, NoDowngradeVerificationResult,
|
|
182
|
+
OperationApplication,
|
|
183
|
+
};
|
|
184
|
+
pub use persistence_reality::{
|
|
185
|
+
CorruptionDetectionResult, DurablePersistenceLayer, FsyncDurabilityResult,
|
|
186
|
+
PersistenceCrashPoint, PersistenceContractVerifier, RecoveryResult, WriteResult, WALEntry,
|
|
187
|
+
};
|
|
188
|
+
pub use adversarial_transport::{
|
|
189
|
+
AdversarialTransport, ConnectionId, ConnectivityMatrix, FaultSeed, InMemoryTransport,
|
|
190
|
+
LinkState, MessageId, NetworkEvent, NetworkSchedule, NodeId, ReplicationTransport,
|
|
191
|
+
};
|
|
192
|
+
pub use materialization::{MaterializationHandler, ReactiveCollectionIntegration};
|
|
193
|
+
pub use operation::{MutationOrder, Operation, OperationType, DEFAULT_MUTATION_ORDER};
|
|
194
|
+
pub use operation_log::{MemoryOperationLog, OperationLogEntry};
|
|
195
|
+
pub use state_transition_store::{MemoryStateTransitionStore, StateTransitionRecord};
|
|
196
|
+
pub use transaction_api::{TransactionManager, TxError, TxResult};
|
|
197
|
+
pub use peer_registry::PeerRegistry;
|
|
198
|
+
pub use provenance::{
|
|
199
|
+
AcquisitionMetadata, DerivedState, DerivedStateProvenance, DerivedValue, OperationSource,
|
|
200
|
+
};
|
|
201
|
+
pub use references::{
|
|
202
|
+
ContentIdentity, ContentRange, FlowRef, PeerAdvertisement, PeerId, ReferenceResolver,
|
|
203
|
+
ResolutionMetadata, ResolutionResult,
|
|
204
|
+
};
|
|
205
|
+
pub use routing::{
|
|
206
|
+
CapabilityRouter, ExecutionClaim, ExecutionClaimer, ExecutionResultPublisher, PeerStateVersion,
|
|
207
|
+
RouteDecision,
|
|
208
|
+
};
|
|
209
|
+
pub use storage::{CheckpointData, FileStorage, MemoryStorage, Storage};
|
|
210
|
+
pub use sync::{ChangeLog, Conflict, ConflictDetector, PeerState, SyncMessage, SyncState};
|
|
211
|
+
pub use trigger::{Trigger, TriggerFilter, TriggerRegistry};
|
|
212
|
+
pub use workflow::{
|
|
213
|
+
BlockedReason, WorkflowGraph, WorkflowInstance, WorkflowNode, WorkflowOperation, WorkflowRef,
|
|
214
|
+
WorkflowState, WorkflowStepRef, WorkflowStepState,
|
|
215
|
+
};
|
|
216
|
+
pub use workflow_integration::{
|
|
217
|
+
WorkflowCronTrigger, WorkflowIntegration, WorkflowProvenance, WorkflowTrigger,
|
|
218
|
+
};
|
|
219
|
+
pub use workflow_runtime::{WorkflowOperationEntry, WorkflowRuntime};
|
|
220
|
+
|
|
221
|
+
use serde::de::DeserializeOwned;
|
|
222
|
+
use serde::{Deserialize, Serialize};
|
|
223
|
+
use serde_json::Value;
|
|
224
|
+
use sha2::{Digest, Sha256};
|
|
225
|
+
use std::any::type_name;
|
|
226
|
+
use std::collections::hash_map::DefaultHasher;
|
|
227
|
+
use std::collections::{HashMap, HashSet};
|
|
228
|
+
use std::fmt::{Display, Formatter};
|
|
229
|
+
use std::fs::{self, OpenOptions};
|
|
230
|
+
use std::hash::{Hash, Hasher};
|
|
231
|
+
use std::io::{BufRead, BufReader, Write};
|
|
232
|
+
use std::path::{Path, PathBuf};
|
|
233
|
+
use std::sync::{Arc, Mutex};
|
|
234
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
235
|
+
use tokio::sync::broadcast;
|
|
236
|
+
|
|
237
|
+
pub type Result<T> = std::result::Result<T, FlowError>;
|
|
238
|
+
|
|
239
|
+
#[derive(Debug)]
|
|
240
|
+
pub enum FlowError {
|
|
241
|
+
Io(std::io::Error),
|
|
242
|
+
Serde(serde_json::Error),
|
|
243
|
+
CorruptLogLine(String),
|
|
244
|
+
CapabilityError(String),
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
impl Display for FlowError {
|
|
248
|
+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
249
|
+
match self {
|
|
250
|
+
FlowError::Io(e) => write!(f, "io error: {e}"),
|
|
251
|
+
FlowError::Serde(e) => write!(f, "serde error: {e}"),
|
|
252
|
+
FlowError::CorruptLogLine(line) => write!(f, "corrupt log line: {line}"),
|
|
253
|
+
FlowError::CapabilityError(msg) => write!(f, "capability error: {msg}"),
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
impl std::error::Error for FlowError {}
|
|
259
|
+
|
|
260
|
+
impl From<std::io::Error> for FlowError {
|
|
261
|
+
fn from(value: std::io::Error) -> Self {
|
|
262
|
+
FlowError::Io(value)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
impl From<serde_json::Error> for FlowError {
|
|
267
|
+
fn from(value: serde_json::Error) -> Self {
|
|
268
|
+
FlowError::Serde(value)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
impl From<String> for FlowError {
|
|
273
|
+
fn from(value: String) -> Self {
|
|
274
|
+
FlowError::CapabilityError(value)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
#[derive(Clone)]
|
|
279
|
+
pub struct FeltDb {
|
|
280
|
+
inner: Arc<Mutex<Inner>>,
|
|
281
|
+
event_tx: broadcast::Sender<ChangeEvent>,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
#[derive(Default)]
|
|
285
|
+
#[allow(dead_code)] // Reserved runtime subsystems are initialized before their public APIs land.
|
|
286
|
+
struct Inner {
|
|
287
|
+
path: PathBuf,
|
|
288
|
+
rows: HashMap<String, HashMap<String, StoredRow>>,
|
|
289
|
+
key_counters: HashMap<String, u64>,
|
|
290
|
+
query_count_by_type: HashMap<String, usize>,
|
|
291
|
+
adaptive_indexes: HashSet<String>,
|
|
292
|
+
instance_id: String,
|
|
293
|
+
sequence: u64,
|
|
294
|
+
sync_state: SyncState,
|
|
295
|
+
change_log: ChangeLog,
|
|
296
|
+
execution_queue: ExecutionQueue,
|
|
297
|
+
trigger_registry: TriggerRegistry,
|
|
298
|
+
cron_scheduler: CronScheduler,
|
|
299
|
+
capability_registry: CapabilityRegistry,
|
|
300
|
+
peer_registry: PeerRegistry,
|
|
301
|
+
content_store: ContentAddressableStore,
|
|
302
|
+
capability_locations: CapabilityLocationRegistry,
|
|
303
|
+
bootstrapped: bool,
|
|
304
|
+
applied_transactions: HashSet<String>,
|
|
305
|
+
collection_cardinality: HashMap<String, u64>,
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
309
|
+
pub struct AtomicMutation {
|
|
310
|
+
pub capability: String,
|
|
311
|
+
pub key: String,
|
|
312
|
+
pub value: Option<Value>,
|
|
313
|
+
}
|
|
314
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
315
|
+
pub struct AtomicPrecondition {
|
|
316
|
+
pub capability: String,
|
|
317
|
+
pub key: String,
|
|
318
|
+
pub expected_version: Option<u64>,
|
|
319
|
+
}
|
|
320
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
321
|
+
pub struct AtomicCommit {
|
|
322
|
+
pub transaction_id: String,
|
|
323
|
+
pub state_before: u64,
|
|
324
|
+
pub state_after: u64,
|
|
325
|
+
pub rows: Vec<StoredRow>,
|
|
326
|
+
pub duplicate: bool,
|
|
327
|
+
}
|
|
328
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
329
|
+
struct TransactionLogRecord {
|
|
330
|
+
record_type: String,
|
|
331
|
+
transaction_id: String,
|
|
332
|
+
state_before: u64,
|
|
333
|
+
state_after: u64,
|
|
334
|
+
rows: Vec<StoredRow>,
|
|
335
|
+
#[serde(default)]
|
|
336
|
+
audit: Option<Value>,
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/// An event representing a change to the database (insert or update of a record).
|
|
340
|
+
///
|
|
341
|
+
/// This event is broadcast to all live query subscribers so they can update their results.
|
|
342
|
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
343
|
+
pub struct ChangeEvent {
|
|
344
|
+
/// The capability namespace of the changed record.
|
|
345
|
+
pub capability: String,
|
|
346
|
+
/// The key of the changed record.
|
|
347
|
+
pub key: String,
|
|
348
|
+
/// The Rust type name of the changed record.
|
|
349
|
+
pub rust_type: String,
|
|
350
|
+
/// The timestamp of the change in milliseconds since UNIX epoch.
|
|
351
|
+
pub unix_ms: u128,
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
355
|
+
pub struct StoredRow {
|
|
356
|
+
pub capability: String,
|
|
357
|
+
pub key: String,
|
|
358
|
+
pub rust_type: String,
|
|
359
|
+
pub value: Value,
|
|
360
|
+
pub unix_ms: u128,
|
|
361
|
+
/// Content hash for deduplication and content addressing
|
|
362
|
+
#[serde(default)]
|
|
363
|
+
pub content_hash: Option<String>,
|
|
364
|
+
/// Reference identity if this row represents a referenced object
|
|
365
|
+
#[serde(default)]
|
|
366
|
+
pub flow_ref: Option<FlowRef>,
|
|
367
|
+
/// Tombstone marker used to durably replay deletions.
|
|
368
|
+
#[serde(default)]
|
|
369
|
+
pub deleted: bool,
|
|
370
|
+
/// Canonical mutation which produced this row. Older logs remain readable.
|
|
371
|
+
#[serde(default)]
|
|
372
|
+
pub operation: Option<Operation>,
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
376
|
+
pub struct DatabaseSnapshot {
|
|
377
|
+
pub rows: Vec<StoredRow>,
|
|
378
|
+
pub versions: HashMap<String, u64>,
|
|
379
|
+
pub content_hash: String,
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
impl DatabaseSnapshot {
|
|
383
|
+
fn compute_hash(rows: &[StoredRow], versions: &HashMap<String, u64>) -> Result<String> {
|
|
384
|
+
let mut rows = rows.to_vec();
|
|
385
|
+
rows.sort_by(|left, right| left.key.cmp(&right.key));
|
|
386
|
+
let versions: std::collections::BTreeMap<_, _> = versions.iter().collect();
|
|
387
|
+
let mut hasher = Sha256::new();
|
|
388
|
+
hasher.update(serde_json::to_vec(&(rows, versions))?);
|
|
389
|
+
Ok(format!("{:x}", hasher.finalize()))
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
pub fn verify(&self) -> Result<()> {
|
|
393
|
+
if self.content_hash != Self::compute_hash(&self.rows, &self.versions)? {
|
|
394
|
+
return Err(FlowError::CapabilityError(
|
|
395
|
+
"snapshot content hash mismatch".to_string(),
|
|
396
|
+
));
|
|
397
|
+
}
|
|
398
|
+
Ok(())
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
403
|
+
struct LogHeader {
|
|
404
|
+
record_type: String,
|
|
405
|
+
local_sequence: u64,
|
|
406
|
+
observed_versions: HashMap<String, u64>,
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
#[derive(Debug, Default, Serialize, Deserialize)]
|
|
410
|
+
struct SyncMetadata {
|
|
411
|
+
local_sequence: u64,
|
|
412
|
+
observed_versions: HashMap<String, u64>,
|
|
413
|
+
peer_acknowledgements: HashMap<String, HashMap<String, u64>>,
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
pub fn open<P: AsRef<Path>>(path: P) -> Result<FeltDb> {
|
|
417
|
+
FeltDb::open(path)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
impl FeltDb {
|
|
421
|
+
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
|
|
422
|
+
let path = path.as_ref().to_path_buf();
|
|
423
|
+
if let Some(parent) = path.parent() {
|
|
424
|
+
fs::create_dir_all(parent)?;
|
|
425
|
+
}
|
|
426
|
+
if !path.exists() {
|
|
427
|
+
OpenOptions::new().create(true).append(true).open(&path)?;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Generate instance ID from path hash
|
|
431
|
+
let instance_id = format!("feltdb-{:x}", {
|
|
432
|
+
let mut hasher = DefaultHasher::new();
|
|
433
|
+
path.to_string_lossy().hash(&mut hasher);
|
|
434
|
+
hasher.finish()
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
let mut inner = Inner {
|
|
438
|
+
path: path.clone(),
|
|
439
|
+
instance_id: instance_id.clone(),
|
|
440
|
+
..Inner::default()
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// Initialize sync state with instance ID
|
|
444
|
+
inner.sync_state = SyncState::new(instance_id);
|
|
445
|
+
replay_log(&mut inner)?;
|
|
446
|
+
load_sync_metadata(&mut inner)?;
|
|
447
|
+
|
|
448
|
+
let (event_tx, _) = broadcast::channel(1024);
|
|
449
|
+
|
|
450
|
+
Ok(Self {
|
|
451
|
+
inner: Arc::new(Mutex::new(inner)),
|
|
452
|
+
event_tx,
|
|
453
|
+
})
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
pub fn insert<T: Serialize>(&self, key: &str, value: T) -> Result<()> {
|
|
457
|
+
let capability = key
|
|
458
|
+
.split_once(':')
|
|
459
|
+
.map(|(cap, _)| cap)
|
|
460
|
+
.unwrap_or("default")
|
|
461
|
+
.to_string();
|
|
462
|
+
self.insert_internal(capability, key.to_string(), value)?;
|
|
463
|
+
Ok(())
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/// Update an existing record with vector clock tracking
|
|
467
|
+
pub fn update<T: Serialize>(&self, key: &str, value: T) -> Result<()> {
|
|
468
|
+
let capability = key
|
|
469
|
+
.split_once(':')
|
|
470
|
+
.map(|(cap, _)| cap)
|
|
471
|
+
.unwrap_or("default")
|
|
472
|
+
.to_string();
|
|
473
|
+
self.update_internal(capability, key.to_string(), value)?;
|
|
474
|
+
Ok(())
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/// Delete a record with vector clock tracking
|
|
478
|
+
pub fn delete(&self, key: &str) -> Result<()> {
|
|
479
|
+
let capability = key
|
|
480
|
+
.split_once(':')
|
|
481
|
+
.map(|(cap, _)| cap)
|
|
482
|
+
.unwrap_or("default")
|
|
483
|
+
.to_string();
|
|
484
|
+
self.delete_internal(capability, key.to_string())?;
|
|
485
|
+
Ok(())
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
|
|
489
|
+
let capability = key
|
|
490
|
+
.split_once(':')
|
|
491
|
+
.map(|(cap, _)| cap)
|
|
492
|
+
.unwrap_or("default")
|
|
493
|
+
.to_string();
|
|
494
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
495
|
+
let value = inner
|
|
496
|
+
.rows
|
|
497
|
+
.get(&capability)
|
|
498
|
+
.and_then(|bucket| bucket.get(key))
|
|
499
|
+
.map(|row| serde_json::from_value::<T>(row.value.clone()))
|
|
500
|
+
.transpose()?;
|
|
501
|
+
Ok(value)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
pub fn query<T, F>(&self, predicate: F) -> Result<Vec<T>>
|
|
505
|
+
where
|
|
506
|
+
T: DeserializeOwned,
|
|
507
|
+
F: Fn(&T) -> bool,
|
|
508
|
+
{
|
|
509
|
+
let rust_type = type_name::<T>().to_string();
|
|
510
|
+
let mut inner = self.inner.lock().expect("lock poisoned");
|
|
511
|
+
*inner
|
|
512
|
+
.query_count_by_type
|
|
513
|
+
.entry(rust_type.clone())
|
|
514
|
+
.or_default() += 1;
|
|
515
|
+
if inner.query_count_by_type[&rust_type] >= 3 {
|
|
516
|
+
inner.adaptive_indexes.insert(rust_type.clone());
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
let mut out = Vec::new();
|
|
520
|
+
for bucket in inner.rows.values() {
|
|
521
|
+
for row in bucket.values() {
|
|
522
|
+
if row.rust_type != rust_type {
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
let parsed = serde_json::from_value::<T>(row.value.clone())?;
|
|
526
|
+
if predicate(&parsed) {
|
|
527
|
+
out.push(parsed);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
Ok(out)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
pub fn capability(&self, name: &str) -> Capability {
|
|
535
|
+
Capability {
|
|
536
|
+
db: self.clone(),
|
|
537
|
+
name: name.to_string(),
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
pub fn is_auto_indexed<T>(&self) -> bool {
|
|
542
|
+
let rust_type = type_name::<T>().to_string();
|
|
543
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
544
|
+
inner.adaptive_indexes.contains(&rust_type)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/// Get the instance ID for this FeltDB instance
|
|
548
|
+
pub fn instance_id(&self) -> Result<String> {
|
|
549
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
550
|
+
Ok(inner.instance_id.clone())
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/// Get the current sequence number for this instance
|
|
554
|
+
pub fn sequence(&self) -> Result<u64> {
|
|
555
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
556
|
+
Ok(inner.sequence)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/// Get the maintained cardinality (record count) for a collection capability.
|
|
560
|
+
/// Returns the count of non-deleted records in the collection.
|
|
561
|
+
pub fn collection_cardinality(&self, capability: &str) -> Result<u64> {
|
|
562
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
563
|
+
Ok(inner.collection_cardinality.get(capability).copied().unwrap_or(0))
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/// Diagnostic: List all collection capabilities and their cardinalities.
|
|
567
|
+
/// Used to detect capability format mismatches.
|
|
568
|
+
pub fn list_cardinalities(&self) -> Result<Vec<(String, u64)>> {
|
|
569
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
570
|
+
let mut result: Vec<_> = inner
|
|
571
|
+
.collection_cardinality
|
|
572
|
+
.iter()
|
|
573
|
+
.map(|(k, v)| (k.clone(), *v))
|
|
574
|
+
.collect();
|
|
575
|
+
result.sort_by(|a, b| b.1.cmp(&a.1)); // Sort by count descending
|
|
576
|
+
Ok(result)
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/// Diagnostic: Count actual rows for a given capability pattern.
|
|
580
|
+
/// Used to verify that rows exist even if cardinality is 0.
|
|
581
|
+
pub fn diagnostic_row_count(&self, capability_pattern: &str) -> Result<usize> {
|
|
582
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
583
|
+
Ok(inner
|
|
584
|
+
.rows
|
|
585
|
+
.iter()
|
|
586
|
+
.filter(|(cap, _)| cap.contains(capability_pattern))
|
|
587
|
+
.map(|(_, bucket)| bucket.values().filter(|row| !row.deleted).count())
|
|
588
|
+
.sum())
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/// Diagnostic: Get actual persisted capability strings matching a pattern.
|
|
592
|
+
/// Used to identify capability identity formats in use.
|
|
593
|
+
pub fn diagnostic_capability_keys(&self, capability_pattern: &str) -> Result<Vec<String>> {
|
|
594
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
595
|
+
let mut keys: Vec<_> = inner
|
|
596
|
+
.rows
|
|
597
|
+
.keys()
|
|
598
|
+
.filter(|k| k.contains(capability_pattern))
|
|
599
|
+
.cloned()
|
|
600
|
+
.collect();
|
|
601
|
+
keys.sort();
|
|
602
|
+
Ok(keys)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
pub fn has_applied_transaction(&self, transaction_id: &str) -> Result<bool> {
|
|
606
|
+
Ok(self
|
|
607
|
+
.inner
|
|
608
|
+
.lock()
|
|
609
|
+
.expect("lock poisoned")
|
|
610
|
+
.applied_transactions
|
|
611
|
+
.contains(transaction_id))
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/// Atomically validates and durably applies an ordered mutation batch.
|
|
615
|
+
/// The complete transaction is represented by one log record and becomes
|
|
616
|
+
/// visible in memory only after that record has been appended successfully.
|
|
617
|
+
pub fn apply_atomic_transaction(
|
|
618
|
+
&self,
|
|
619
|
+
transaction_id: &str,
|
|
620
|
+
expected_parent: Option<u64>,
|
|
621
|
+
preconditions: &[AtomicPrecondition],
|
|
622
|
+
mutations: &[AtomicMutation],
|
|
623
|
+
audit: Option<Value>,
|
|
624
|
+
) -> Result<AtomicCommit> {
|
|
625
|
+
let (commit, events) = {
|
|
626
|
+
let mut inner = self.inner.lock().expect("lock poisoned");
|
|
627
|
+
if inner.applied_transactions.contains(transaction_id) {
|
|
628
|
+
return Ok(AtomicCommit {
|
|
629
|
+
transaction_id: transaction_id.into(),
|
|
630
|
+
state_before: inner.sequence,
|
|
631
|
+
state_after: inner.sequence,
|
|
632
|
+
rows: vec![],
|
|
633
|
+
duplicate: true,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
if let Some(expected) = expected_parent {
|
|
637
|
+
if expected != inner.sequence {
|
|
638
|
+
return Err(FlowError::CapabilityError(format!(
|
|
639
|
+
"PRECONDITION_FAILED:transaction_parent:{expected}:{}",
|
|
640
|
+
inner.sequence
|
|
641
|
+
)));
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
for condition in preconditions {
|
|
645
|
+
let actual = inner
|
|
646
|
+
.rows
|
|
647
|
+
.get(&condition.capability)
|
|
648
|
+
.and_then(|bucket| bucket.get(&condition.key))
|
|
649
|
+
.and_then(|row| row.operation.as_ref())
|
|
650
|
+
.map(|op| op.sequence);
|
|
651
|
+
if actual != condition.expected_version {
|
|
652
|
+
return Err(FlowError::CapabilityError(format!(
|
|
653
|
+
"PRECONDITION_FAILED:{}:{}:{:?}:{actual:?}",
|
|
654
|
+
condition.capability, condition.key, condition.expected_version
|
|
655
|
+
)));
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
let state_before = inner.sequence;
|
|
659
|
+
let mut next_sequence = state_before;
|
|
660
|
+
let mut rows = Vec::with_capacity(mutations.len());
|
|
661
|
+
let mut vector_clock = inner.sync_state.get_or_init_vector_clock().clone();
|
|
662
|
+
for mutation in mutations {
|
|
663
|
+
next_sequence += 1;
|
|
664
|
+
*vector_clock.entry(inner.instance_id.clone()).or_default() += 1;
|
|
665
|
+
let rust_type = "serde_json::Value".to_string();
|
|
666
|
+
let operation = match &mutation.value {
|
|
667
|
+
Some(value)
|
|
668
|
+
if inner
|
|
669
|
+
.rows
|
|
670
|
+
.get(&mutation.capability)
|
|
671
|
+
.is_some_and(|bucket| bucket.contains_key(&mutation.key)) =>
|
|
672
|
+
{
|
|
673
|
+
Operation::update(
|
|
674
|
+
next_sequence,
|
|
675
|
+
inner.instance_id.clone(),
|
|
676
|
+
next_sequence,
|
|
677
|
+
mutation.key.clone(),
|
|
678
|
+
value.clone(),
|
|
679
|
+
rust_type.clone(),
|
|
680
|
+
mutation.capability.clone(),
|
|
681
|
+
)
|
|
682
|
+
}
|
|
683
|
+
Some(value) => Operation::insert(
|
|
684
|
+
next_sequence,
|
|
685
|
+
inner.instance_id.clone(),
|
|
686
|
+
next_sequence,
|
|
687
|
+
mutation.key.clone(),
|
|
688
|
+
value.clone(),
|
|
689
|
+
rust_type.clone(),
|
|
690
|
+
mutation.capability.clone(),
|
|
691
|
+
),
|
|
692
|
+
None => Operation::delete(
|
|
693
|
+
next_sequence,
|
|
694
|
+
inner.instance_id.clone(),
|
|
695
|
+
next_sequence,
|
|
696
|
+
mutation.key.clone(),
|
|
697
|
+
rust_type.clone(),
|
|
698
|
+
mutation.capability.clone(),
|
|
699
|
+
),
|
|
700
|
+
}
|
|
701
|
+
.with_vector_clock(vector_clock.clone());
|
|
702
|
+
rows.push(StoredRow {
|
|
703
|
+
capability: mutation.capability.clone(),
|
|
704
|
+
key: mutation.key.clone(),
|
|
705
|
+
rust_type,
|
|
706
|
+
value: mutation.value.clone().unwrap_or(Value::Null),
|
|
707
|
+
unix_ms: now_ms(),
|
|
708
|
+
content_hash: Some(operation.content_hash.clone()),
|
|
709
|
+
flow_ref: None,
|
|
710
|
+
deleted: mutation.value.is_none(),
|
|
711
|
+
operation: Some(operation),
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
let mut audit = audit;
|
|
715
|
+
if let Some(object) = audit.as_mut().and_then(Value::as_object_mut) {
|
|
716
|
+
object.insert("state_before".into(), Value::from(state_before));
|
|
717
|
+
object.insert("state_after".into(), Value::from(next_sequence));
|
|
718
|
+
}
|
|
719
|
+
let record = TransactionLogRecord {
|
|
720
|
+
record_type: "feltdb.transaction.v1".into(),
|
|
721
|
+
transaction_id: transaction_id.into(),
|
|
722
|
+
state_before,
|
|
723
|
+
state_after: next_sequence,
|
|
724
|
+
rows: rows.clone(),
|
|
725
|
+
audit,
|
|
726
|
+
};
|
|
727
|
+
append_transaction(&inner.path, &record)?;
|
|
728
|
+
inner.sequence = next_sequence;
|
|
729
|
+
for row in &rows {
|
|
730
|
+
if let Some(operation) = row.operation.clone() {
|
|
731
|
+
inner.change_log.add_operation(operation);
|
|
732
|
+
}
|
|
733
|
+
if row.deleted {
|
|
734
|
+
if let Some(bucket) = inner.rows.get_mut(&row.capability) {
|
|
735
|
+
bucket.remove(&row.key);
|
|
736
|
+
}
|
|
737
|
+
// Decrement cardinality on delete
|
|
738
|
+
let count = inner.collection_cardinality.entry(row.capability.clone()).or_default();
|
|
739
|
+
*count = count.saturating_sub(1);
|
|
740
|
+
} else {
|
|
741
|
+
// Track whether this is an insert (new row) or update (existing row)
|
|
742
|
+
let is_insert = !inner
|
|
743
|
+
.rows
|
|
744
|
+
.get(&row.capability)
|
|
745
|
+
.is_some_and(|bucket| bucket.contains_key(&row.key));
|
|
746
|
+
|
|
747
|
+
inner
|
|
748
|
+
.rows
|
|
749
|
+
.entry(row.capability.clone())
|
|
750
|
+
.or_default()
|
|
751
|
+
.insert(row.key.clone(), row.clone());
|
|
752
|
+
|
|
753
|
+
// Increment cardinality only on insert, not update
|
|
754
|
+
if is_insert {
|
|
755
|
+
let count = inner.collection_cardinality.entry(row.capability.clone()).or_default();
|
|
756
|
+
*count = count.saturating_add(1);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
inner.sync_state.merge_vector_clock(&vector_clock);
|
|
761
|
+
inner.applied_transactions.insert(transaction_id.into());
|
|
762
|
+
let events = rows
|
|
763
|
+
.iter()
|
|
764
|
+
.map(|row| ChangeEvent {
|
|
765
|
+
capability: row.capability.clone(),
|
|
766
|
+
key: row.key.clone(),
|
|
767
|
+
rust_type: row.rust_type.clone(),
|
|
768
|
+
unix_ms: row.unix_ms,
|
|
769
|
+
})
|
|
770
|
+
.collect::<Vec<_>>();
|
|
771
|
+
(
|
|
772
|
+
AtomicCommit {
|
|
773
|
+
transaction_id: transaction_id.into(),
|
|
774
|
+
state_before,
|
|
775
|
+
state_after: inner.sequence,
|
|
776
|
+
rows,
|
|
777
|
+
duplicate: false,
|
|
778
|
+
},
|
|
779
|
+
events,
|
|
780
|
+
)
|
|
781
|
+
};
|
|
782
|
+
for event in events {
|
|
783
|
+
let _ = self.event_tx.send(event);
|
|
784
|
+
}
|
|
785
|
+
Ok(commit)
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
fn insert_internal<T: Serialize>(
|
|
789
|
+
&self,
|
|
790
|
+
capability: String,
|
|
791
|
+
key: String,
|
|
792
|
+
value: T,
|
|
793
|
+
) -> Result<()> {
|
|
794
|
+
let rust_type = type_name::<T>().to_string();
|
|
795
|
+
let value_json = serde_json::to_value(value)?;
|
|
796
|
+
let mut row = StoredRow {
|
|
797
|
+
capability: capability.clone(),
|
|
798
|
+
key: key.clone(),
|
|
799
|
+
rust_type: rust_type.clone(),
|
|
800
|
+
value: value_json.clone(),
|
|
801
|
+
unix_ms: now_ms(),
|
|
802
|
+
content_hash: None,
|
|
803
|
+
flow_ref: None,
|
|
804
|
+
deleted: false,
|
|
805
|
+
operation: None,
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
{
|
|
809
|
+
let mut inner = self.inner.lock().expect("lock poisoned");
|
|
810
|
+
|
|
811
|
+
// Increment sequence number
|
|
812
|
+
inner.sequence += 1;
|
|
813
|
+
|
|
814
|
+
// Increment vector clock for this instance
|
|
815
|
+
inner.sync_state.increment_vector_clock();
|
|
816
|
+
|
|
817
|
+
// Get current vector clock to attach to operation
|
|
818
|
+
let vector_clock = inner.sync_state.get_or_init_vector_clock().clone();
|
|
819
|
+
|
|
820
|
+
// Create an operation with vector clock for convergence
|
|
821
|
+
let op = Operation::insert(
|
|
822
|
+
inner.sequence,
|
|
823
|
+
inner.instance_id.clone(),
|
|
824
|
+
inner.sequence,
|
|
825
|
+
key.clone(),
|
|
826
|
+
value_json,
|
|
827
|
+
rust_type.clone(),
|
|
828
|
+
capability.clone(),
|
|
829
|
+
)
|
|
830
|
+
.with_vector_clock(vector_clock);
|
|
831
|
+
|
|
832
|
+
// Add operation to change log for sync
|
|
833
|
+
row.operation = Some(op.clone());
|
|
834
|
+
inner.change_log.add_operation(op.clone());
|
|
835
|
+
|
|
836
|
+
append_event(&inner.path, &row)?;
|
|
837
|
+
inner
|
|
838
|
+
.rows
|
|
839
|
+
.entry(capability.clone())
|
|
840
|
+
.or_default()
|
|
841
|
+
.insert(key.clone(), row);
|
|
842
|
+
let suffix = key.rsplit_once(':').map(|(_, right)| right);
|
|
843
|
+
if let Some(id) = suffix.and_then(|s| s.parse::<u64>().ok()) {
|
|
844
|
+
let entry = inner.key_counters.entry(capability.clone()).or_default();
|
|
845
|
+
*entry = (*entry).max(id + 1);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
let event = ChangeEvent {
|
|
850
|
+
capability,
|
|
851
|
+
key,
|
|
852
|
+
rust_type,
|
|
853
|
+
unix_ms: now_ms(),
|
|
854
|
+
};
|
|
855
|
+
let _ = self.event_tx.send(event);
|
|
856
|
+
|
|
857
|
+
Ok(())
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
fn update_internal<T: Serialize>(
|
|
861
|
+
&self,
|
|
862
|
+
capability: String,
|
|
863
|
+
key: String,
|
|
864
|
+
value: T,
|
|
865
|
+
) -> Result<()> {
|
|
866
|
+
let rust_type = type_name::<T>().to_string();
|
|
867
|
+
let value_json = serde_json::to_value(value)?;
|
|
868
|
+
let mut row = StoredRow {
|
|
869
|
+
capability: capability.clone(),
|
|
870
|
+
key: key.clone(),
|
|
871
|
+
rust_type: rust_type.clone(),
|
|
872
|
+
value: value_json.clone(),
|
|
873
|
+
unix_ms: now_ms(),
|
|
874
|
+
content_hash: None,
|
|
875
|
+
flow_ref: None,
|
|
876
|
+
deleted: false,
|
|
877
|
+
operation: None,
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
{
|
|
881
|
+
let mut inner = self.inner.lock().expect("lock poisoned");
|
|
882
|
+
|
|
883
|
+
// Increment sequence number
|
|
884
|
+
inner.sequence += 1;
|
|
885
|
+
|
|
886
|
+
// Increment vector clock for this instance
|
|
887
|
+
inner.sync_state.increment_vector_clock();
|
|
888
|
+
|
|
889
|
+
// Get current vector clock to attach to operation
|
|
890
|
+
let vector_clock = inner.sync_state.get_or_init_vector_clock().clone();
|
|
891
|
+
|
|
892
|
+
// Create an update operation with vector clock for convergence
|
|
893
|
+
let op = Operation::update(
|
|
894
|
+
inner.sequence,
|
|
895
|
+
inner.instance_id.clone(),
|
|
896
|
+
inner.sequence,
|
|
897
|
+
key.clone(),
|
|
898
|
+
value_json,
|
|
899
|
+
rust_type.clone(),
|
|
900
|
+
capability.clone(),
|
|
901
|
+
)
|
|
902
|
+
.with_vector_clock(vector_clock);
|
|
903
|
+
|
|
904
|
+
// Add operation to change log for sync
|
|
905
|
+
row.operation = Some(op.clone());
|
|
906
|
+
inner.change_log.add_operation(op.clone());
|
|
907
|
+
|
|
908
|
+
append_event(&inner.path, &row)?;
|
|
909
|
+
inner
|
|
910
|
+
.rows
|
|
911
|
+
.entry(capability.clone())
|
|
912
|
+
.or_default()
|
|
913
|
+
.insert(key.clone(), row);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
let event = ChangeEvent {
|
|
917
|
+
capability,
|
|
918
|
+
key,
|
|
919
|
+
rust_type,
|
|
920
|
+
unix_ms: now_ms(),
|
|
921
|
+
};
|
|
922
|
+
let _ = self.event_tx.send(event);
|
|
923
|
+
|
|
924
|
+
Ok(())
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
fn delete_internal(&self, capability: String, key: String) -> Result<()> {
|
|
928
|
+
{
|
|
929
|
+
let mut inner = self.inner.lock().expect("lock poisoned");
|
|
930
|
+
|
|
931
|
+
// Get the row to determine its type
|
|
932
|
+
let rust_type = inner
|
|
933
|
+
.rows
|
|
934
|
+
.get(&capability)
|
|
935
|
+
.and_then(|bucket| bucket.get(&key))
|
|
936
|
+
.map(|row| row.rust_type.clone())
|
|
937
|
+
.unwrap_or_else(|| "Unknown".to_string());
|
|
938
|
+
|
|
939
|
+
// Increment sequence number
|
|
940
|
+
inner.sequence += 1;
|
|
941
|
+
|
|
942
|
+
// Increment vector clock for this instance
|
|
943
|
+
inner.sync_state.increment_vector_clock();
|
|
944
|
+
|
|
945
|
+
// Get current vector clock to attach to operation
|
|
946
|
+
let vector_clock = inner.sync_state.get_or_init_vector_clock().clone();
|
|
947
|
+
|
|
948
|
+
// Create a delete operation with vector clock for convergence
|
|
949
|
+
let op = Operation::delete(
|
|
950
|
+
inner.sequence,
|
|
951
|
+
inner.instance_id.clone(),
|
|
952
|
+
inner.sequence,
|
|
953
|
+
key.clone(),
|
|
954
|
+
rust_type.clone(),
|
|
955
|
+
capability.clone(),
|
|
956
|
+
)
|
|
957
|
+
.with_vector_clock(vector_clock);
|
|
958
|
+
|
|
959
|
+
// Add operation to change log for sync
|
|
960
|
+
inner.change_log.add_operation(op.clone());
|
|
961
|
+
|
|
962
|
+
// Persist a tombstone through the same append-only log used by
|
|
963
|
+
// inserts and updates. Without this, deleted rows reappeared after
|
|
964
|
+
// reopening the database.
|
|
965
|
+
let tombstone = StoredRow {
|
|
966
|
+
capability: capability.clone(),
|
|
967
|
+
key: key.clone(),
|
|
968
|
+
rust_type,
|
|
969
|
+
value: serde_json::Value::Null,
|
|
970
|
+
unix_ms: now_ms(),
|
|
971
|
+
content_hash: None,
|
|
972
|
+
flow_ref: None,
|
|
973
|
+
deleted: true,
|
|
974
|
+
operation: Some(op),
|
|
975
|
+
};
|
|
976
|
+
append_event(&inner.path, &tombstone)?;
|
|
977
|
+
|
|
978
|
+
// Remove from in-memory store
|
|
979
|
+
if let Some(bucket) = inner.rows.get_mut(&capability) {
|
|
980
|
+
bucket.remove(&key);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
let event = ChangeEvent {
|
|
985
|
+
capability,
|
|
986
|
+
key,
|
|
987
|
+
rust_type: "Deleted".to_string(),
|
|
988
|
+
unix_ms: now_ms(),
|
|
989
|
+
};
|
|
990
|
+
let _ = self.event_tx.send(event);
|
|
991
|
+
|
|
992
|
+
Ok(())
|
|
993
|
+
}
|
|
994
|
+
///
|
|
995
|
+
/// The subscription returns an initial set of matching results, and provides a stream
|
|
996
|
+
/// of updated results whenever records matching the predicate are inserted or updated.
|
|
997
|
+
///
|
|
998
|
+
/// # Example
|
|
999
|
+
///
|
|
1000
|
+
/// ```ignore
|
|
1001
|
+
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
|
1002
|
+
/// use feltdb::open;
|
|
1003
|
+
/// let db = open("app.data")?;
|
|
1004
|
+
/// let mut subscription = db.live_query::<User, _>(|u| u.active).await?;
|
|
1005
|
+
/// let initial_users = subscription.initial_results();
|
|
1006
|
+
///
|
|
1007
|
+
/// while let Some(users) = subscription.next().await {
|
|
1008
|
+
/// println!("Active users: {:?}", users);
|
|
1009
|
+
/// }
|
|
1010
|
+
/// # Ok(())
|
|
1011
|
+
/// # }
|
|
1012
|
+
/// ```
|
|
1013
|
+
pub async fn live_query<T, F>(&self, predicate: F) -> Result<LiveQuerySubscription<T>>
|
|
1014
|
+
where
|
|
1015
|
+
T: DeserializeOwned + Send + Sync + 'static,
|
|
1016
|
+
F: Fn(&T) -> bool + Send + Sync + 'static,
|
|
1017
|
+
{
|
|
1018
|
+
let rust_type = type_name::<T>().to_string();
|
|
1019
|
+
|
|
1020
|
+
let initial_results = {
|
|
1021
|
+
let mut inner = self.inner.lock().expect("lock poisoned");
|
|
1022
|
+
*inner
|
|
1023
|
+
.query_count_by_type
|
|
1024
|
+
.entry(rust_type.clone())
|
|
1025
|
+
.or_default() += 1;
|
|
1026
|
+
if inner.query_count_by_type[&rust_type] >= 3 {
|
|
1027
|
+
inner.adaptive_indexes.insert(rust_type.clone());
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
let mut out = Vec::new();
|
|
1031
|
+
for bucket in inner.rows.values() {
|
|
1032
|
+
for row in bucket.values() {
|
|
1033
|
+
if row.rust_type != rust_type {
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
let parsed = serde_json::from_value::<T>(row.value.clone())?;
|
|
1037
|
+
if predicate(&parsed) {
|
|
1038
|
+
out.push(parsed);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
out
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
let rx = self.event_tx.subscribe();
|
|
1046
|
+
let db = self.clone();
|
|
1047
|
+
let subscription = LiveQuerySubscription {
|
|
1048
|
+
initial_results,
|
|
1049
|
+
rx,
|
|
1050
|
+
db,
|
|
1051
|
+
rust_type,
|
|
1052
|
+
predicate: Arc::new(Mutex::new(None)),
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
Ok(subscription)
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/// Get current sync state information
|
|
1059
|
+
pub fn sync_info(&self) -> Result<SyncState> {
|
|
1060
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
1061
|
+
Ok(inner.sync_state.clone())
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/// Read an untyped value for protocol adapters such as the HTTP server.
|
|
1065
|
+
pub fn get_value(&self, key: &str) -> Result<Option<Value>> {
|
|
1066
|
+
self.get::<Value>(key)
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/// List the canonical values in one collection/capability namespace.
|
|
1070
|
+
pub fn list_collection(&self, capability: &str) -> Result<Vec<StoredRow>> {
|
|
1071
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
1072
|
+
Ok(inner
|
|
1073
|
+
.rows
|
|
1074
|
+
.get(capability)
|
|
1075
|
+
.map(|rows| rows.values().cloned().collect())
|
|
1076
|
+
.unwrap_or_default())
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/// Subscribe to canonical mutation events.
|
|
1080
|
+
pub fn subscribe_changes(&self) -> broadcast::Receiver<ChangeEvent> {
|
|
1081
|
+
self.event_tx.subscribe()
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/// Add a peer for synchronization
|
|
1085
|
+
pub fn add_sync_peer(&self, peer_id: String) -> Result<()> {
|
|
1086
|
+
let mut inner = self.inner.lock().expect("lock poisoned");
|
|
1087
|
+
inner.sync_state.add_peer(peer_id);
|
|
1088
|
+
Ok(())
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/// Remove a peer from synchronization
|
|
1092
|
+
pub fn remove_sync_peer(&self, peer_id: &str) -> Result<()> {
|
|
1093
|
+
let mut inner = self.inner.lock().expect("lock poisoned");
|
|
1094
|
+
inner.sync_state.remove_peer(peer_id);
|
|
1095
|
+
Ok(())
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/// Get pending operations for a peer
|
|
1099
|
+
pub fn get_pending_for_peer(
|
|
1100
|
+
&self,
|
|
1101
|
+
_peer_id: &str,
|
|
1102
|
+
since_sequence: u64,
|
|
1103
|
+
) -> Result<Vec<Operation>> {
|
|
1104
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
1105
|
+
let pending: Vec<Operation> = inner
|
|
1106
|
+
.change_log
|
|
1107
|
+
.pending_operations()
|
|
1108
|
+
.into_iter()
|
|
1109
|
+
.filter(|op| op.sequence > since_sequence)
|
|
1110
|
+
.collect();
|
|
1111
|
+
Ok(pending)
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/// Return operations missing from a peer's per-origin version vector.
|
|
1115
|
+
pub fn operations_since(
|
|
1116
|
+
&self,
|
|
1117
|
+
versions: &std::collections::HashMap<String, u64>,
|
|
1118
|
+
) -> Result<Vec<Operation>> {
|
|
1119
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
1120
|
+
Ok(inner.change_log.operations_since(versions))
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/// Return the highest durable operation sequence observed from each origin.
|
|
1124
|
+
pub fn operation_versions(&self) -> Result<std::collections::HashMap<String, u64>> {
|
|
1125
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
1126
|
+
Ok(inner.change_log.versions())
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
pub fn export_snapshot(&self) -> Result<DatabaseSnapshot> {
|
|
1130
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
1131
|
+
let mut rows: Vec<StoredRow> = inner
|
|
1132
|
+
.rows
|
|
1133
|
+
.values()
|
|
1134
|
+
.flat_map(|bucket| bucket.values().cloned())
|
|
1135
|
+
.collect();
|
|
1136
|
+
for row in &mut rows {
|
|
1137
|
+
row.operation = None;
|
|
1138
|
+
}
|
|
1139
|
+
rows.sort_by(|left, right| left.key.cmp(&right.key));
|
|
1140
|
+
let versions = inner.change_log.versions();
|
|
1141
|
+
let content_hash = DatabaseSnapshot::compute_hash(&rows, &versions)?;
|
|
1142
|
+
Ok(DatabaseSnapshot {
|
|
1143
|
+
rows,
|
|
1144
|
+
versions,
|
|
1145
|
+
content_hash,
|
|
1146
|
+
})
|
|
1147
|
+
}
|
|
1148
|
+
/// Current materialized rows including their causal operation envelopes.
|
|
1149
|
+
/// State-contract execution uses this for record-version preconditions;
|
|
1150
|
+
/// portable snapshots intentionally continue to strip those envelopes.
|
|
1151
|
+
pub fn state_rows(&self) -> Result<Vec<StoredRow>> {
|
|
1152
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
1153
|
+
Ok(inner
|
|
1154
|
+
.rows
|
|
1155
|
+
.values()
|
|
1156
|
+
.flat_map(|bucket| bucket.values().cloned())
|
|
1157
|
+
.collect())
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
pub fn can_install_snapshot(&self) -> Result<bool> {
|
|
1161
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
1162
|
+
Ok(!inner.bootstrapped && inner.rows.is_empty() && inner.change_log.pending.is_empty())
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/// Install a verified bootstrap snapshot into a pristine node. A node with
|
|
1166
|
+
/// local history must converge through operations rather than overwrite it.
|
|
1167
|
+
pub fn install_snapshot(&self, snapshot: DatabaseSnapshot) -> Result<()> {
|
|
1168
|
+
snapshot.verify()?;
|
|
1169
|
+
let mut inner = self.inner.lock().expect("lock poisoned");
|
|
1170
|
+
if !inner.rows.is_empty() || !inner.change_log.pending.is_empty() {
|
|
1171
|
+
return Err(FlowError::CapabilityError(
|
|
1172
|
+
"snapshot bootstrap requires a pristine node".to_string(),
|
|
1173
|
+
));
|
|
1174
|
+
}
|
|
1175
|
+
for row in &snapshot.rows {
|
|
1176
|
+
if row.deleted
|
|
1177
|
+
|| row.operation.is_some()
|
|
1178
|
+
|| !row.key.starts_with(&format!("{}:", row.capability))
|
|
1179
|
+
{
|
|
1180
|
+
return Err(FlowError::CapabilityError(
|
|
1181
|
+
"snapshot contains an invalid state row".to_string(),
|
|
1182
|
+
));
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
let temporary = inner.path.with_extension("snapshot.tmp");
|
|
1187
|
+
let mut file = OpenOptions::new()
|
|
1188
|
+
.create(true)
|
|
1189
|
+
.write(true)
|
|
1190
|
+
.truncate(true)
|
|
1191
|
+
.open(&temporary)?;
|
|
1192
|
+
write_json_line(
|
|
1193
|
+
&mut file,
|
|
1194
|
+
&LogHeader {
|
|
1195
|
+
record_type: "feltdb.snapshot.v1".to_string(),
|
|
1196
|
+
local_sequence: inner.sequence,
|
|
1197
|
+
observed_versions: snapshot.versions.clone(),
|
|
1198
|
+
},
|
|
1199
|
+
)?;
|
|
1200
|
+
for row in &snapshot.rows {
|
|
1201
|
+
write_json_line(&mut file, row)?;
|
|
1202
|
+
}
|
|
1203
|
+
file.flush()?;
|
|
1204
|
+
file.sync_all()?;
|
|
1205
|
+
fs::rename(temporary, &inner.path)?;
|
|
1206
|
+
|
|
1207
|
+
inner.rows.clear();
|
|
1208
|
+
inner.key_counters.clear();
|
|
1209
|
+
for row in snapshot.rows {
|
|
1210
|
+
if let Some(id) = row
|
|
1211
|
+
.key
|
|
1212
|
+
.rsplit_once(':')
|
|
1213
|
+
.and_then(|(_, right)| right.parse::<u64>().ok())
|
|
1214
|
+
{
|
|
1215
|
+
let counter = inner
|
|
1216
|
+
.key_counters
|
|
1217
|
+
.entry(row.capability.clone())
|
|
1218
|
+
.or_default();
|
|
1219
|
+
*counter = (*counter).max(id + 1);
|
|
1220
|
+
}
|
|
1221
|
+
inner
|
|
1222
|
+
.rows
|
|
1223
|
+
.entry(row.capability.clone())
|
|
1224
|
+
.or_default()
|
|
1225
|
+
.insert(row.key.clone(), row);
|
|
1226
|
+
}
|
|
1227
|
+
inner.change_log.observed_versions = snapshot.versions;
|
|
1228
|
+
inner.bootstrapped = true;
|
|
1229
|
+
persist_sync_metadata(&inner)
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/// Acknowledge receipt of operations from a peer
|
|
1233
|
+
pub fn acknowledge_peer_operations(&self, peer_id: String, sequence: u64) -> Result<()> {
|
|
1234
|
+
let mut inner = self.inner.lock().expect("lock poisoned");
|
|
1235
|
+
inner.change_log.acknowledge_peer(peer_id, sequence);
|
|
1236
|
+
persist_sync_metadata(&inner)?;
|
|
1237
|
+
Ok(())
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/// Durably record a peer's per-origin acknowledgement vector.
|
|
1241
|
+
pub fn acknowledge_peer_versions(
|
|
1242
|
+
&self,
|
|
1243
|
+
peer_id: String,
|
|
1244
|
+
versions: HashMap<String, u64>,
|
|
1245
|
+
) -> Result<()> {
|
|
1246
|
+
let mut inner = self.inner.lock().expect("lock poisoned");
|
|
1247
|
+
inner.change_log.acknowledge_versions(peer_id, versions);
|
|
1248
|
+
persist_sync_metadata(&inner)
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
pub fn acknowledged_peer_versions(&self, peer_id: &str) -> Result<HashMap<String, u64>> {
|
|
1252
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
1253
|
+
Ok(inner.change_log.acknowledged_versions(peer_id))
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
pub fn acknowledged_operation_count(&self, active_peers: &[String]) -> Result<usize> {
|
|
1257
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
1258
|
+
Ok(inner.change_log.acknowledged_operation_count(active_peers))
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/// Atomically replace acknowledged history with a state snapshot and any
|
|
1262
|
+
/// operations still needed by at least one configured peer.
|
|
1263
|
+
pub fn compact_operation_log(&self, active_peers: &[String]) -> Result<usize> {
|
|
1264
|
+
let mut inner = self.inner.lock().expect("lock poisoned");
|
|
1265
|
+
let removed = inner.change_log.prune_acknowledged(active_peers);
|
|
1266
|
+
if removed == 0 {
|
|
1267
|
+
return Ok(0);
|
|
1268
|
+
}
|
|
1269
|
+
persist_sync_metadata(&inner)?;
|
|
1270
|
+
|
|
1271
|
+
let temporary = inner.path.with_extension("compact.tmp");
|
|
1272
|
+
let mut file = OpenOptions::new()
|
|
1273
|
+
.create(true)
|
|
1274
|
+
.write(true)
|
|
1275
|
+
.truncate(true)
|
|
1276
|
+
.open(&temporary)?;
|
|
1277
|
+
write_json_line(
|
|
1278
|
+
&mut file,
|
|
1279
|
+
&LogHeader {
|
|
1280
|
+
record_type: "feltdb.snapshot.v1".to_string(),
|
|
1281
|
+
local_sequence: inner.sequence,
|
|
1282
|
+
observed_versions: inner.change_log.versions(),
|
|
1283
|
+
},
|
|
1284
|
+
)?;
|
|
1285
|
+
for operation in inner.change_log.pending_operations() {
|
|
1286
|
+
write_json_line(&mut file, &stored_row_for_operation(operation))?;
|
|
1287
|
+
}
|
|
1288
|
+
for bucket in inner.rows.values() {
|
|
1289
|
+
for row in bucket.values() {
|
|
1290
|
+
let mut snapshot_row = row.clone();
|
|
1291
|
+
snapshot_row.operation = None;
|
|
1292
|
+
write_json_line(&mut file, &snapshot_row)?;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
file.flush()?;
|
|
1296
|
+
file.sync_all()?;
|
|
1297
|
+
fs::rename(temporary, &inner.path)?;
|
|
1298
|
+
inner.bootstrapped = true;
|
|
1299
|
+
Ok(removed)
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
/// Register a peer in the distributed fabric
|
|
1303
|
+
pub fn register_peer(&self, advertisement: PeerAdvertisement) -> Result<()> {
|
|
1304
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
1305
|
+
inner
|
|
1306
|
+
.peer_registry
|
|
1307
|
+
.register(advertisement)
|
|
1308
|
+
.map_err(|e| FlowError::CapabilityError(e))
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/// Get a peer's advertisement
|
|
1312
|
+
pub fn get_peer(&self, peer_id: &PeerId) -> Result<Option<PeerAdvertisement>> {
|
|
1313
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
1314
|
+
inner
|
|
1315
|
+
.peer_registry
|
|
1316
|
+
.get(peer_id)
|
|
1317
|
+
.map_err(|e| FlowError::CapabilityError(e))
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/// Find all peers providing a namespace
|
|
1321
|
+
pub fn peers_for_namespace(&self, namespace: &str) -> Result<Vec<PeerAdvertisement>> {
|
|
1322
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
1323
|
+
inner
|
|
1324
|
+
.peer_registry
|
|
1325
|
+
.peers_for_namespace(namespace)
|
|
1326
|
+
.map_err(|e| FlowError::CapabilityError(e))
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/// Find all peers providing a capability
|
|
1330
|
+
pub fn peers_for_capability(&self, capability: &str) -> Result<Vec<PeerAdvertisement>> {
|
|
1331
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
1332
|
+
inner
|
|
1333
|
+
.peer_registry
|
|
1334
|
+
.peers_for_capability(capability)
|
|
1335
|
+
.map_err(|e| FlowError::CapabilityError(e))
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/// Get all registered peers
|
|
1339
|
+
pub fn all_peers(&self) -> Result<Vec<PeerAdvertisement>> {
|
|
1340
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
1341
|
+
inner
|
|
1342
|
+
.peer_registry
|
|
1343
|
+
.all_peers()
|
|
1344
|
+
.map_err(|e| FlowError::CapabilityError(e))
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
/// Resolve a FlowRef locally
|
|
1348
|
+
pub fn resolve_ref<T: DeserializeOwned>(&self, flow_ref: &FlowRef) -> Result<Option<T>> {
|
|
1349
|
+
// First, try to resolve locally
|
|
1350
|
+
let key = format!("{}:{}", flow_ref.namespace, flow_ref.object_id);
|
|
1351
|
+
self.get::<T>(&key)
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/// Get current sync state
|
|
1355
|
+
pub fn sync_state(&self) -> Result<SyncState> {
|
|
1356
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
1357
|
+
Ok(inner.sync_state.clone())
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/// Apply an operation from a remote peer
|
|
1361
|
+
/// This merges the vector clock, resolves conflicts, and applies the mutation
|
|
1362
|
+
pub fn apply_remote_operation(&self, op: Operation) -> Result<()> {
|
|
1363
|
+
let mut inner = self.inner.lock().expect("lock poisoned");
|
|
1364
|
+
|
|
1365
|
+
if op.content_hash != op.compute_content_hash() {
|
|
1366
|
+
return Err(FlowError::CapabilityError(
|
|
1367
|
+
"remote operation content hash mismatch".to_string(),
|
|
1368
|
+
));
|
|
1369
|
+
}
|
|
1370
|
+
if inner.change_log.has_operation(&op.instance_id, op.sequence) {
|
|
1371
|
+
return Ok(());
|
|
1372
|
+
}
|
|
1373
|
+
let expected = inner
|
|
1374
|
+
.change_log
|
|
1375
|
+
.versions()
|
|
1376
|
+
.get(&op.instance_id)
|
|
1377
|
+
.copied()
|
|
1378
|
+
.unwrap_or(0)
|
|
1379
|
+
+ 1;
|
|
1380
|
+
if op.sequence != expected {
|
|
1381
|
+
return Err(FlowError::CapabilityError(format!(
|
|
1382
|
+
"remote operation sequence gap for {}: expected {}, received {}",
|
|
1383
|
+
op.instance_id, expected, op.sequence
|
|
1384
|
+
)));
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// Merge the remote vector clock into our own
|
|
1388
|
+
if let Some(remote_clock) = &op.vector_clock {
|
|
1389
|
+
inner.sync_state.merge_vector_clock(remote_clock);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// Check if we have a local operation for this key that hasn't been synced yet
|
|
1393
|
+
let local_op_for_key = inner
|
|
1394
|
+
.change_log
|
|
1395
|
+
.pending_operations()
|
|
1396
|
+
.into_iter()
|
|
1397
|
+
.find(|local_op| local_op.key == op.key && local_op.capability == op.capability);
|
|
1398
|
+
|
|
1399
|
+
// Determine which operation to apply (remote or local if conflict)
|
|
1400
|
+
let op_to_apply = if let Some(local_op) = local_op_for_key {
|
|
1401
|
+
// We have a conflict - use LWW with vector clock awareness
|
|
1402
|
+
// Create causal histories for conflict resolution
|
|
1403
|
+
let local_vc = local_op.vector_clock.clone().unwrap_or_default();
|
|
1404
|
+
let remote_vc = op.vector_clock.clone().unwrap_or_default();
|
|
1405
|
+
|
|
1406
|
+
let local_history = CausalHistory::new(
|
|
1407
|
+
StateVersion::from_clocks(local_vc, local_op.timestamp_ms),
|
|
1408
|
+
local_op.instance_id.clone(),
|
|
1409
|
+
local_op.sequence,
|
|
1410
|
+
);
|
|
1411
|
+
|
|
1412
|
+
let remote_history = CausalHistory::new(
|
|
1413
|
+
StateVersion::from_clocks(remote_vc, op.timestamp_ms),
|
|
1414
|
+
op.instance_id.clone(),
|
|
1415
|
+
op.sequence,
|
|
1416
|
+
);
|
|
1417
|
+
|
|
1418
|
+
// Resolve using LWW strategy
|
|
1419
|
+
let resolution = ConflictResolution::resolve_lww(&[local_history, remote_history]);
|
|
1420
|
+
|
|
1421
|
+
// Winner index: 0 = local, 1 = remote
|
|
1422
|
+
if resolution.winner_index == Some(1) {
|
|
1423
|
+
op.clone()
|
|
1424
|
+
} else {
|
|
1425
|
+
// The local value wins, but the received operation still belongs
|
|
1426
|
+
// to the durable causal history so peers do not resend it forever.
|
|
1427
|
+
if let Some(mut history_row) = inner
|
|
1428
|
+
.rows
|
|
1429
|
+
.get(&op.capability)
|
|
1430
|
+
.and_then(|rows| rows.get(&op.key))
|
|
1431
|
+
.cloned()
|
|
1432
|
+
{
|
|
1433
|
+
history_row.operation = Some(op.clone());
|
|
1434
|
+
append_event(&inner.path, &history_row)?;
|
|
1435
|
+
}
|
|
1436
|
+
inner.change_log.add_operation(op);
|
|
1437
|
+
inner.sync_state.increment_received(1);
|
|
1438
|
+
return Ok(());
|
|
1439
|
+
}
|
|
1440
|
+
} else {
|
|
1441
|
+
op.clone()
|
|
1442
|
+
};
|
|
1443
|
+
|
|
1444
|
+
// Apply the winning operation to our state
|
|
1445
|
+
let row = StoredRow {
|
|
1446
|
+
capability: op_to_apply.capability.clone(),
|
|
1447
|
+
key: op_to_apply.key.clone(),
|
|
1448
|
+
rust_type: op_to_apply.rust_type.clone(),
|
|
1449
|
+
value: op_to_apply.value.clone().unwrap_or(serde_json::Value::Null),
|
|
1450
|
+
unix_ms: op_to_apply.timestamp_ms,
|
|
1451
|
+
content_hash: Some(op_to_apply.content_hash.clone()),
|
|
1452
|
+
flow_ref: None,
|
|
1453
|
+
deleted: op_to_apply.op_type == OperationType::Delete,
|
|
1454
|
+
operation: Some(op.clone()),
|
|
1455
|
+
};
|
|
1456
|
+
|
|
1457
|
+
match op_to_apply.op_type {
|
|
1458
|
+
OperationType::Insert | OperationType::Update => {
|
|
1459
|
+
append_event(&inner.path, &row)?;
|
|
1460
|
+
inner
|
|
1461
|
+
.rows
|
|
1462
|
+
.entry(op_to_apply.capability.clone())
|
|
1463
|
+
.or_default()
|
|
1464
|
+
.insert(op_to_apply.key.clone(), row);
|
|
1465
|
+
}
|
|
1466
|
+
OperationType::Delete => {
|
|
1467
|
+
append_event(&inner.path, &row)?;
|
|
1468
|
+
if let Some(bucket) = inner.rows.get_mut(&op_to_apply.capability) {
|
|
1469
|
+
bucket.remove(&op_to_apply.key);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
if let Some(id) = op_to_apply
|
|
1474
|
+
.key
|
|
1475
|
+
.rsplit_once(':')
|
|
1476
|
+
.and_then(|(_, right)| right.parse::<u64>().ok())
|
|
1477
|
+
{
|
|
1478
|
+
let counter = inner
|
|
1479
|
+
.key_counters
|
|
1480
|
+
.entry(op_to_apply.capability.clone())
|
|
1481
|
+
.or_default();
|
|
1482
|
+
*counter = (*counter).max(id + 1);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
inner.change_log.add_operation(op);
|
|
1486
|
+
inner.sync_state.increment_received(1);
|
|
1487
|
+
let event = ChangeEvent {
|
|
1488
|
+
capability: op_to_apply.capability,
|
|
1489
|
+
key: op_to_apply.key,
|
|
1490
|
+
rust_type: op_to_apply.rust_type,
|
|
1491
|
+
unix_ms: op_to_apply.timestamp_ms,
|
|
1492
|
+
};
|
|
1493
|
+
drop(inner);
|
|
1494
|
+
let _ = self.event_tx.send(event);
|
|
1495
|
+
|
|
1496
|
+
Ok(())
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
/// Get the current vector clock state
|
|
1500
|
+
pub fn get_vector_clock(&self) -> Result<Option<std::collections::HashMap<String, u64>>> {
|
|
1501
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
1502
|
+
Ok(inner.sync_state.vector_clock.clone())
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
#[derive(Clone)]
|
|
1507
|
+
pub struct Capability {
|
|
1508
|
+
db: FeltDb,
|
|
1509
|
+
name: String,
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
impl Capability {
|
|
1513
|
+
pub fn insert<T: Serialize>(&self, value: T) -> Result<String> {
|
|
1514
|
+
let key = {
|
|
1515
|
+
let mut inner = self.db.inner.lock().expect("lock poisoned");
|
|
1516
|
+
let next = inner.key_counters.entry(self.name.clone()).or_default();
|
|
1517
|
+
let key = format!("{}:{}", self.name, *next);
|
|
1518
|
+
*next += 1;
|
|
1519
|
+
key
|
|
1520
|
+
};
|
|
1521
|
+
self.db
|
|
1522
|
+
.insert_internal(self.name.clone(), key.clone(), value)?;
|
|
1523
|
+
Ok(key)
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
pub fn insert_with_key<T: Serialize>(&self, key: &str, value: T) -> Result<()> {
|
|
1527
|
+
let full_key = if key.contains(':') {
|
|
1528
|
+
key.to_string()
|
|
1529
|
+
} else {
|
|
1530
|
+
format!("{}:{key}", self.name)
|
|
1531
|
+
};
|
|
1532
|
+
self.db.insert_internal(self.name.clone(), full_key, value)
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
pub fn update<T: Serialize>(&self, key: &str, value: T) -> Result<()> {
|
|
1536
|
+
let full_key = if key.contains(':') {
|
|
1537
|
+
key.to_string()
|
|
1538
|
+
} else {
|
|
1539
|
+
format!("{}:{key}", self.name)
|
|
1540
|
+
};
|
|
1541
|
+
self.db.update_internal(self.name.clone(), full_key, value)
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
pub fn delete(&self, key: &str) -> Result<()> {
|
|
1545
|
+
let full_key = if key.contains(':') {
|
|
1546
|
+
key.to_string()
|
|
1547
|
+
} else {
|
|
1548
|
+
format!("{}:{key}", self.name)
|
|
1549
|
+
};
|
|
1550
|
+
self.db.delete_internal(self.name.clone(), full_key)
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
|
|
1554
|
+
let full_key = if key.contains(':') {
|
|
1555
|
+
key.to_string()
|
|
1556
|
+
} else {
|
|
1557
|
+
format!("{}:{key}", self.name)
|
|
1558
|
+
};
|
|
1559
|
+
self.db.get(&full_key)
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
pub fn find<T, F>(&self, predicate: F) -> Result<Vec<T>>
|
|
1563
|
+
where
|
|
1564
|
+
T: DeserializeOwned,
|
|
1565
|
+
F: Fn(&T) -> bool,
|
|
1566
|
+
{
|
|
1567
|
+
let rust_type = type_name::<T>().to_string();
|
|
1568
|
+
let mut inner = self.db.inner.lock().expect("lock poisoned");
|
|
1569
|
+
*inner
|
|
1570
|
+
.query_count_by_type
|
|
1571
|
+
.entry(rust_type.clone())
|
|
1572
|
+
.or_default() += 1;
|
|
1573
|
+
if inner.query_count_by_type[&rust_type] >= 3 {
|
|
1574
|
+
inner.adaptive_indexes.insert(rust_type.clone());
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
let mut out = Vec::new();
|
|
1578
|
+
if let Some(bucket) = inner.rows.get(&self.name) {
|
|
1579
|
+
for row in bucket.values() {
|
|
1580
|
+
if row.rust_type != rust_type {
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1583
|
+
let parsed = serde_json::from_value::<T>(row.value.clone())?;
|
|
1584
|
+
if predicate(&parsed) {
|
|
1585
|
+
out.push(parsed);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
Ok(out)
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/// Creates a live query subscription scoped to this capability.
|
|
1593
|
+
///
|
|
1594
|
+
/// The subscription returns initial matching results from this capability only, and streams
|
|
1595
|
+
/// updates whenever records within this capability matching the predicate are inserted or updated.
|
|
1596
|
+
///
|
|
1597
|
+
/// # Example
|
|
1598
|
+
///
|
|
1599
|
+
/// ```ignore
|
|
1600
|
+
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
|
1601
|
+
/// use feltdb::open;
|
|
1602
|
+
/// let db = open("app.data")?;
|
|
1603
|
+
/// let users = db.capability("users");
|
|
1604
|
+
/// let mut subscription = users.live_find::<User, _>(|u| u.active).await?;
|
|
1605
|
+
/// let initial_users = subscription.initial_results();
|
|
1606
|
+
///
|
|
1607
|
+
/// while let Some(users) = subscription.next().await {
|
|
1608
|
+
/// println!("Active users: {:?}", users);
|
|
1609
|
+
/// }
|
|
1610
|
+
/// # Ok(())
|
|
1611
|
+
/// # }
|
|
1612
|
+
/// ```
|
|
1613
|
+
pub async fn live_find<T, F>(&self, predicate: F) -> Result<LiveCapabilitySubscription<T>>
|
|
1614
|
+
where
|
|
1615
|
+
T: DeserializeOwned + Send + Sync + 'static,
|
|
1616
|
+
F: Fn(&T) -> bool + Send + Sync + 'static,
|
|
1617
|
+
{
|
|
1618
|
+
let rust_type = type_name::<T>().to_string();
|
|
1619
|
+
let capability_name = self.name.clone();
|
|
1620
|
+
|
|
1621
|
+
let initial_results = {
|
|
1622
|
+
let mut inner = self.db.inner.lock().expect("lock poisoned");
|
|
1623
|
+
*inner
|
|
1624
|
+
.query_count_by_type
|
|
1625
|
+
.entry(rust_type.clone())
|
|
1626
|
+
.or_default() += 1;
|
|
1627
|
+
if inner.query_count_by_type[&rust_type] >= 3 {
|
|
1628
|
+
inner.adaptive_indexes.insert(rust_type.clone());
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
let mut out = Vec::new();
|
|
1632
|
+
if let Some(bucket) = inner.rows.get(&capability_name) {
|
|
1633
|
+
for row in bucket.values() {
|
|
1634
|
+
if row.rust_type != rust_type {
|
|
1635
|
+
continue;
|
|
1636
|
+
}
|
|
1637
|
+
let parsed = serde_json::from_value::<T>(row.value.clone())?;
|
|
1638
|
+
if predicate(&parsed) {
|
|
1639
|
+
out.push(parsed);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
out
|
|
1644
|
+
};
|
|
1645
|
+
|
|
1646
|
+
let rx = self.db.event_tx.subscribe();
|
|
1647
|
+
let subscription = LiveCapabilitySubscription {
|
|
1648
|
+
initial_results,
|
|
1649
|
+
rx,
|
|
1650
|
+
db: self.db.clone(),
|
|
1651
|
+
capability_name,
|
|
1652
|
+
rust_type,
|
|
1653
|
+
predicate: Arc::new(Mutex::new(None)),
|
|
1654
|
+
};
|
|
1655
|
+
|
|
1656
|
+
Ok(subscription)
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// Type alias for boxed predicates
|
|
1661
|
+
type PredicateBox<T> = Arc<Mutex<Option<Box<dyn Fn(&T) -> bool + Send + Sync>>>>;
|
|
1662
|
+
|
|
1663
|
+
/// A subscription to a live query that streams updates whenever matching records change.
|
|
1664
|
+
///
|
|
1665
|
+
/// This struct holds the initial query results and provides a stream interface to receive
|
|
1666
|
+
/// updates whenever records matching the query predicate are inserted or updated.
|
|
1667
|
+
pub struct LiveQuerySubscription<T> {
|
|
1668
|
+
/// The initial set of records matching the query predicate at subscription time.
|
|
1669
|
+
pub initial_results: Vec<T>,
|
|
1670
|
+
rx: broadcast::Receiver<ChangeEvent>,
|
|
1671
|
+
db: FeltDb,
|
|
1672
|
+
rust_type: String,
|
|
1673
|
+
predicate: PredicateBox<T>,
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
impl<T> LiveQuerySubscription<T>
|
|
1677
|
+
where
|
|
1678
|
+
T: DeserializeOwned + Send + Sync + 'static,
|
|
1679
|
+
{
|
|
1680
|
+
pub fn set_predicate<F>(&mut self, predicate: F)
|
|
1681
|
+
where
|
|
1682
|
+
F: Fn(&T) -> bool + Send + Sync + 'static,
|
|
1683
|
+
{
|
|
1684
|
+
*self.predicate.lock().expect("lock poisoned") = Some(Box::new(predicate));
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
/// Waits for the next change event and returns the updated query results.
|
|
1688
|
+
///
|
|
1689
|
+
/// This method is async and will block until a change event is received for the
|
|
1690
|
+
/// subscribed type. When an event arrives, it re-evaluates the full dataset and
|
|
1691
|
+
/// returns all matching records.
|
|
1692
|
+
///
|
|
1693
|
+
/// Returns `None` if the broadcast channel has been dropped (all senders are gone).
|
|
1694
|
+
pub async fn next(&mut self) -> Option<Vec<T>> {
|
|
1695
|
+
loop {
|
|
1696
|
+
match self.rx.recv().await {
|
|
1697
|
+
Ok(event) => {
|
|
1698
|
+
if event.rust_type != self.rust_type {
|
|
1699
|
+
continue;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
let results = {
|
|
1703
|
+
let inner = self.db.inner.lock().expect("lock poisoned");
|
|
1704
|
+
let mut out = Vec::new();
|
|
1705
|
+
for bucket in inner.rows.values() {
|
|
1706
|
+
for row in bucket.values() {
|
|
1707
|
+
if row.rust_type != self.rust_type {
|
|
1708
|
+
continue;
|
|
1709
|
+
}
|
|
1710
|
+
if let Ok(parsed) = serde_json::from_value::<T>(row.value.clone()) {
|
|
1711
|
+
out.push(parsed);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
out
|
|
1716
|
+
};
|
|
1717
|
+
|
|
1718
|
+
return Some(results);
|
|
1719
|
+
}
|
|
1720
|
+
Err(_) => {
|
|
1721
|
+
return None;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/// Returns a reference to the initial query results captured at subscription time.
|
|
1728
|
+
///
|
|
1729
|
+
/// This provides access to the baseline set of matching records before any changes
|
|
1730
|
+
/// have been streamed through `next()`.
|
|
1731
|
+
pub fn initial_results(&self) -> &[T] {
|
|
1732
|
+
&self.initial_results
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
/// A capability-scoped subscription to a live query that streams updates for records within a specific capability.
|
|
1737
|
+
///
|
|
1738
|
+
/// Similar to `LiveQuerySubscription`, but only receives updates for records within the specified capability.
|
|
1739
|
+
/// This provides more granular control over subscription scope.
|
|
1740
|
+
pub struct LiveCapabilitySubscription<T> {
|
|
1741
|
+
pub initial_results: Vec<T>,
|
|
1742
|
+
rx: broadcast::Receiver<ChangeEvent>,
|
|
1743
|
+
db: FeltDb,
|
|
1744
|
+
capability_name: String,
|
|
1745
|
+
rust_type: String,
|
|
1746
|
+
predicate: PredicateBox<T>,
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
impl<T> LiveCapabilitySubscription<T>
|
|
1750
|
+
where
|
|
1751
|
+
T: DeserializeOwned + Send + Sync + 'static,
|
|
1752
|
+
{
|
|
1753
|
+
pub fn set_predicate<F>(&mut self, predicate: F)
|
|
1754
|
+
where
|
|
1755
|
+
F: Fn(&T) -> bool + Send + Sync + 'static,
|
|
1756
|
+
{
|
|
1757
|
+
*self.predicate.lock().expect("lock poisoned") = Some(Box::new(predicate));
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
/// Waits for the next change event within this capability and returns the updated query results.
|
|
1761
|
+
///
|
|
1762
|
+
/// Similar to `LiveQuerySubscription::next()`, but only responds to changes within the
|
|
1763
|
+
/// scoped capability.
|
|
1764
|
+
pub async fn next(&mut self) -> Option<Vec<T>> {
|
|
1765
|
+
loop {
|
|
1766
|
+
match self.rx.recv().await {
|
|
1767
|
+
Ok(event) => {
|
|
1768
|
+
if event.rust_type != self.rust_type || event.capability != self.capability_name
|
|
1769
|
+
{
|
|
1770
|
+
continue;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
let results = {
|
|
1774
|
+
let inner = self.db.inner.lock().expect("lock poisoned");
|
|
1775
|
+
let mut out = Vec::new();
|
|
1776
|
+
if let Some(bucket) = inner.rows.get(&self.capability_name) {
|
|
1777
|
+
for row in bucket.values() {
|
|
1778
|
+
if row.rust_type != self.rust_type {
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
1781
|
+
if let Ok(parsed) = serde_json::from_value::<T>(row.value.clone()) {
|
|
1782
|
+
out.push(parsed);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
out
|
|
1787
|
+
};
|
|
1788
|
+
|
|
1789
|
+
return Some(results);
|
|
1790
|
+
}
|
|
1791
|
+
Err(_) => {
|
|
1792
|
+
return None;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
pub fn initial_results(&self) -> &[T] {
|
|
1799
|
+
&self.initial_results
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
fn replay_log(inner: &mut Inner) -> Result<()> {
|
|
1804
|
+
let file = OpenOptions::new().read(true).open(&inner.path)?;
|
|
1805
|
+
let reader = BufReader::new(file);
|
|
1806
|
+
|
|
1807
|
+
let mut snapshot_log = false;
|
|
1808
|
+
for line in reader.lines() {
|
|
1809
|
+
let line = line?;
|
|
1810
|
+
if line.trim().is_empty() {
|
|
1811
|
+
continue;
|
|
1812
|
+
}
|
|
1813
|
+
let value: Value =
|
|
1814
|
+
serde_json::from_str(&line).map_err(|_| FlowError::CorruptLogLine(line.clone()))?;
|
|
1815
|
+
if matches!(
|
|
1816
|
+
value.get("record_type").and_then(Value::as_str),
|
|
1817
|
+
Some("feltdb.snapshot.v1" | "flowdb.snapshot.v1")
|
|
1818
|
+
) {
|
|
1819
|
+
let header: LogHeader = serde_json::from_value(value)?;
|
|
1820
|
+
inner.sequence = inner.sequence.max(header.local_sequence);
|
|
1821
|
+
inner.change_log.observed_versions = header.observed_versions;
|
|
1822
|
+
inner.bootstrapped = true;
|
|
1823
|
+
snapshot_log = true;
|
|
1824
|
+
continue;
|
|
1825
|
+
}
|
|
1826
|
+
if value.get("record_type").and_then(Value::as_str) == Some("feltdb.transaction.v1") {
|
|
1827
|
+
let transaction: TransactionLogRecord = serde_json::from_value(value)?;
|
|
1828
|
+
if inner
|
|
1829
|
+
.applied_transactions
|
|
1830
|
+
.insert(transaction.transaction_id)
|
|
1831
|
+
{
|
|
1832
|
+
for row in transaction.rows {
|
|
1833
|
+
if let Some(operation) = row.operation.clone() {
|
|
1834
|
+
inner.sequence = inner.sequence.max(operation.sequence);
|
|
1835
|
+
if let Some(clock) = &operation.vector_clock {
|
|
1836
|
+
inner.sync_state.merge_vector_clock(clock);
|
|
1837
|
+
}
|
|
1838
|
+
inner.change_log.add_operation(operation);
|
|
1839
|
+
}
|
|
1840
|
+
if row.deleted {
|
|
1841
|
+
if let Some(bucket) = inner.rows.get_mut(&row.capability) {
|
|
1842
|
+
bucket.remove(&row.key);
|
|
1843
|
+
}
|
|
1844
|
+
} else {
|
|
1845
|
+
inner
|
|
1846
|
+
.rows
|
|
1847
|
+
.entry(row.capability.clone())
|
|
1848
|
+
.or_default()
|
|
1849
|
+
.insert(row.key.clone(), row);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
inner.sequence = inner.sequence.max(transaction.state_after);
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
let row: StoredRow =
|
|
1857
|
+
serde_json::from_value(value).map_err(|_| FlowError::CorruptLogLine(line.clone()))?;
|
|
1858
|
+
if let Some(operation) = row.operation.clone() {
|
|
1859
|
+
inner.sequence = inner.sequence.max(
|
|
1860
|
+
(operation.instance_id == inner.instance_id)
|
|
1861
|
+
.then_some(operation.sequence)
|
|
1862
|
+
.unwrap_or(0),
|
|
1863
|
+
);
|
|
1864
|
+
if let Some(clock) = &operation.vector_clock {
|
|
1865
|
+
inner.sync_state.merge_vector_clock(clock);
|
|
1866
|
+
}
|
|
1867
|
+
inner.change_log.add_operation(operation);
|
|
1868
|
+
} else if !snapshot_log {
|
|
1869
|
+
// Pre-operation-envelope logs were produced only by this local
|
|
1870
|
+
// store. Preserve a monotonic sequence when upgrading them.
|
|
1871
|
+
inner.sequence += 1;
|
|
1872
|
+
}
|
|
1873
|
+
if let Some(id) = row
|
|
1874
|
+
.key
|
|
1875
|
+
.rsplit_once(':')
|
|
1876
|
+
.and_then(|(_, right)| right.parse::<u64>().ok())
|
|
1877
|
+
{
|
|
1878
|
+
let counter = inner
|
|
1879
|
+
.key_counters
|
|
1880
|
+
.entry(row.capability.clone())
|
|
1881
|
+
.or_default();
|
|
1882
|
+
*counter = (*counter).max(id + 1);
|
|
1883
|
+
}
|
|
1884
|
+
if row.deleted {
|
|
1885
|
+
if let Some(bucket) = inner.rows.get_mut(&row.capability) {
|
|
1886
|
+
bucket.remove(&row.key);
|
|
1887
|
+
}
|
|
1888
|
+
} else {
|
|
1889
|
+
inner
|
|
1890
|
+
.rows
|
|
1891
|
+
.entry(row.capability.clone())
|
|
1892
|
+
.or_default()
|
|
1893
|
+
.insert(row.key.clone(), row);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
// Compute collection cardinality from final row state (count non-deleted rows per capability)
|
|
1898
|
+
for (capability, bucket) in &inner.rows {
|
|
1899
|
+
let count = bucket.values().filter(|row| !row.deleted).count() as u64;
|
|
1900
|
+
inner.collection_cardinality.insert(capability.clone(), count);
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
Ok(())
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
fn write_json_line<T: Serialize>(file: &mut std::fs::File, value: &T) -> Result<()> {
|
|
1907
|
+
serde_json::to_writer(&mut *file, value)?;
|
|
1908
|
+
file.write_all(b"\n")?;
|
|
1909
|
+
Ok(())
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
fn stored_row_for_operation(operation: Operation) -> StoredRow {
|
|
1913
|
+
StoredRow {
|
|
1914
|
+
capability: operation.capability.clone(),
|
|
1915
|
+
key: operation.key.clone(),
|
|
1916
|
+
rust_type: operation.rust_type.clone(),
|
|
1917
|
+
value: operation.value.clone().unwrap_or(Value::Null),
|
|
1918
|
+
unix_ms: operation.timestamp_ms,
|
|
1919
|
+
content_hash: Some(operation.content_hash.clone()),
|
|
1920
|
+
flow_ref: None,
|
|
1921
|
+
deleted: operation.op_type == OperationType::Delete,
|
|
1922
|
+
operation: Some(operation),
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
fn sync_metadata_path(path: &Path) -> PathBuf {
|
|
1927
|
+
path.with_extension("sync.json")
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
fn load_sync_metadata(inner: &mut Inner) -> Result<()> {
|
|
1931
|
+
let path = sync_metadata_path(&inner.path);
|
|
1932
|
+
if !path.exists() {
|
|
1933
|
+
return Ok(());
|
|
1934
|
+
}
|
|
1935
|
+
let metadata: SyncMetadata = serde_json::from_slice(&fs::read(path)?)?;
|
|
1936
|
+
inner.sequence = inner.sequence.max(metadata.local_sequence);
|
|
1937
|
+
for (origin, sequence) in metadata.observed_versions {
|
|
1938
|
+
inner
|
|
1939
|
+
.change_log
|
|
1940
|
+
.observed_versions
|
|
1941
|
+
.entry(origin)
|
|
1942
|
+
.and_modify(|current| *current = (*current).max(sequence))
|
|
1943
|
+
.or_insert(sequence);
|
|
1944
|
+
}
|
|
1945
|
+
inner.change_log.peer_acknowledgements = metadata.peer_acknowledgements;
|
|
1946
|
+
Ok(())
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
fn persist_sync_metadata(inner: &Inner) -> Result<()> {
|
|
1950
|
+
let path = sync_metadata_path(&inner.path);
|
|
1951
|
+
let temporary = path.with_extension("sync.tmp");
|
|
1952
|
+
let metadata = SyncMetadata {
|
|
1953
|
+
local_sequence: inner.sequence,
|
|
1954
|
+
observed_versions: inner.change_log.versions(),
|
|
1955
|
+
peer_acknowledgements: inner.change_log.peer_acknowledgements.clone(),
|
|
1956
|
+
};
|
|
1957
|
+
let mut file = OpenOptions::new()
|
|
1958
|
+
.create(true)
|
|
1959
|
+
.write(true)
|
|
1960
|
+
.truncate(true)
|
|
1961
|
+
.open(&temporary)?;
|
|
1962
|
+
file.write_all(&serde_json::to_vec_pretty(&metadata)?)?;
|
|
1963
|
+
file.flush()?;
|
|
1964
|
+
file.sync_all()?;
|
|
1965
|
+
fs::rename(temporary, path)?;
|
|
1966
|
+
Ok(())
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
fn append_event(path: &Path, event: &StoredRow) -> Result<()> {
|
|
1970
|
+
let mut file = OpenOptions::new().create(true).append(true).open(path)?;
|
|
1971
|
+
let mut line = serde_json::to_string(event)?;
|
|
1972
|
+
line.push('\n');
|
|
1973
|
+
file.write_all(line.as_bytes())?;
|
|
1974
|
+
file.flush()?;
|
|
1975
|
+
Ok(())
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
fn append_transaction(path: &Path, transaction: &TransactionLogRecord) -> Result<()> {
|
|
1979
|
+
let mut file = OpenOptions::new().create(true).append(true).open(path)?;
|
|
1980
|
+
write_json_line(&mut file, transaction)?;
|
|
1981
|
+
file.sync_data()?;
|
|
1982
|
+
Ok(())
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
fn now_ms() -> u128 {
|
|
1986
|
+
SystemTime::now()
|
|
1987
|
+
.duration_since(UNIX_EPOCH)
|
|
1988
|
+
.expect("time moved backwards")
|
|
1989
|
+
.as_millis()
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
#[cfg(test)]
|
|
1993
|
+
mod tests {
|
|
1994
|
+
use super::*;
|
|
1995
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
1996
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
1997
|
+
|
|
1998
|
+
static TEMP_FILE_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
|
1999
|
+
|
|
2000
|
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
|
2001
|
+
struct User {
|
|
2002
|
+
email: String,
|
|
2003
|
+
age: u8,
|
|
2004
|
+
active: bool,
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
fn temp_file() -> PathBuf {
|
|
2008
|
+
let nanos = SystemTime::now()
|
|
2009
|
+
.duration_since(UNIX_EPOCH)
|
|
2010
|
+
.expect("time")
|
|
2011
|
+
.as_nanos();
|
|
2012
|
+
let sequence = TEMP_FILE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
|
|
2013
|
+
std::env::temp_dir().join(format!(
|
|
2014
|
+
"feltdb-test-{}-{nanos}-{sequence}.data",
|
|
2015
|
+
std::process::id()
|
|
2016
|
+
))
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
#[test]
|
|
2020
|
+
fn open_is_zero_init_and_creates_store() {
|
|
2021
|
+
let path = temp_file();
|
|
2022
|
+
let db = open(&path).expect("open");
|
|
2023
|
+
assert!(path.exists());
|
|
2024
|
+
drop(db);
|
|
2025
|
+
let _ = fs::remove_file(path);
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
#[test]
|
|
2029
|
+
fn insert_get_and_reopen_are_durable() {
|
|
2030
|
+
let path = temp_file();
|
|
2031
|
+
let db = open(&path).expect("open");
|
|
2032
|
+
let user = User {
|
|
2033
|
+
email: "a@feltdb.dev".to_string(),
|
|
2034
|
+
age: 33,
|
|
2035
|
+
active: true,
|
|
2036
|
+
};
|
|
2037
|
+
db.insert("user:1", &user).expect("insert");
|
|
2038
|
+
let loaded: User = db.get("user:1").expect("get").expect("some");
|
|
2039
|
+
assert_eq!(loaded, user);
|
|
2040
|
+
drop(db);
|
|
2041
|
+
|
|
2042
|
+
let reopened = open(&path).expect("reopen");
|
|
2043
|
+
let loaded_after_restart: User = reopened.get("user:1").expect("get").expect("some");
|
|
2044
|
+
assert_eq!(loaded_after_restart, user);
|
|
2045
|
+
let _ = fs::remove_file(path);
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
#[test]
|
|
2049
|
+
fn delete_and_reopen_preserves_tombstone() {
|
|
2050
|
+
let path = temp_file();
|
|
2051
|
+
let db = open(&path).expect("open");
|
|
2052
|
+
db.insert("users:deleted", serde_json::json!({"name": "gone"}))
|
|
2053
|
+
.expect("insert");
|
|
2054
|
+
db.delete("users:deleted").expect("delete");
|
|
2055
|
+
drop(db);
|
|
2056
|
+
|
|
2057
|
+
let reopened = open(&path).expect("reopen");
|
|
2058
|
+
assert_eq!(reopened.get_value("users:deleted").expect("get"), None);
|
|
2059
|
+
let _ = fs::remove_file(path);
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
#[test]
|
|
2063
|
+
fn closure_query_and_capability_find_work() {
|
|
2064
|
+
let path = temp_file();
|
|
2065
|
+
let db = open(&path).expect("open");
|
|
2066
|
+
let users = db.capability("users");
|
|
2067
|
+
users
|
|
2068
|
+
.insert(User {
|
|
2069
|
+
email: "one@feltdb.dev".to_string(),
|
|
2070
|
+
age: 28,
|
|
2071
|
+
active: true,
|
|
2072
|
+
})
|
|
2073
|
+
.expect("insert");
|
|
2074
|
+
users
|
|
2075
|
+
.insert(User {
|
|
2076
|
+
email: "two@feltdb.dev".to_string(),
|
|
2077
|
+
age: 41,
|
|
2078
|
+
active: false,
|
|
2079
|
+
})
|
|
2080
|
+
.expect("insert");
|
|
2081
|
+
|
|
2082
|
+
let global = db.query::<User, _>(|u| u.age > 30).expect("query");
|
|
2083
|
+
assert_eq!(global.len(), 1);
|
|
2084
|
+
|
|
2085
|
+
let active = users.find::<User, _>(|u| u.active).expect("find");
|
|
2086
|
+
assert_eq!(active.len(), 1);
|
|
2087
|
+
assert_eq!(active[0].email, "one@feltdb.dev");
|
|
2088
|
+
let _ = fs::remove_file(path);
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
#[test]
|
|
2092
|
+
fn adaptive_index_marks_frequent_query_types() {
|
|
2093
|
+
let path = temp_file();
|
|
2094
|
+
let db = open(&path).expect("open");
|
|
2095
|
+
db.insert(
|
|
2096
|
+
"user:1",
|
|
2097
|
+
User {
|
|
2098
|
+
email: "index@feltdb.dev".to_string(),
|
|
2099
|
+
age: 21,
|
|
2100
|
+
active: true,
|
|
2101
|
+
},
|
|
2102
|
+
)
|
|
2103
|
+
.expect("insert");
|
|
2104
|
+
|
|
2105
|
+
for _ in 0..3 {
|
|
2106
|
+
let _ = db.query::<User, _>(|u| u.active).expect("query");
|
|
2107
|
+
}
|
|
2108
|
+
assert!(db.is_auto_indexed::<User>());
|
|
2109
|
+
let _ = fs::remove_file(path);
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
#[tokio::test]
|
|
2113
|
+
async fn live_query_returns_initial_results() {
|
|
2114
|
+
let path = temp_file();
|
|
2115
|
+
let db = open(&path).expect("open");
|
|
2116
|
+
let users = db.capability("users");
|
|
2117
|
+
users
|
|
2118
|
+
.insert(User {
|
|
2119
|
+
email: "live@feltdb.dev".to_string(),
|
|
2120
|
+
age: 28,
|
|
2121
|
+
active: true,
|
|
2122
|
+
})
|
|
2123
|
+
.expect("insert");
|
|
2124
|
+
users
|
|
2125
|
+
.insert(User {
|
|
2126
|
+
email: "inactive@feltdb.dev".to_string(),
|
|
2127
|
+
age: 35,
|
|
2128
|
+
active: false,
|
|
2129
|
+
})
|
|
2130
|
+
.expect("insert");
|
|
2131
|
+
|
|
2132
|
+
let subscription = db
|
|
2133
|
+
.live_query::<User, _>(|u| u.active)
|
|
2134
|
+
.await
|
|
2135
|
+
.expect("live_query");
|
|
2136
|
+
let initial = subscription.initial_results();
|
|
2137
|
+
assert_eq!(initial.len(), 1);
|
|
2138
|
+
assert_eq!(initial[0].email, "live@feltdb.dev");
|
|
2139
|
+
let _ = fs::remove_file(path);
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
#[tokio::test]
|
|
2143
|
+
async fn live_query_streams_new_changes() {
|
|
2144
|
+
let path = temp_file();
|
|
2145
|
+
let db = open(&path).expect("open");
|
|
2146
|
+
let users = db.capability("users");
|
|
2147
|
+
users
|
|
2148
|
+
.insert(User {
|
|
2149
|
+
email: "initial@feltdb.dev".to_string(),
|
|
2150
|
+
age: 28,
|
|
2151
|
+
active: true,
|
|
2152
|
+
})
|
|
2153
|
+
.expect("insert");
|
|
2154
|
+
|
|
2155
|
+
let mut subscription = db
|
|
2156
|
+
.live_query::<User, _>(|u| u.active)
|
|
2157
|
+
.await
|
|
2158
|
+
.expect("live_query");
|
|
2159
|
+
let initial = subscription.initial_results();
|
|
2160
|
+
assert_eq!(initial.len(), 1);
|
|
2161
|
+
|
|
2162
|
+
let db_for_insert = db.clone();
|
|
2163
|
+
tokio::spawn(async move {
|
|
2164
|
+
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
|
2165
|
+
let users_inner = db_for_insert.capability("users");
|
|
2166
|
+
users_inner
|
|
2167
|
+
.insert(User {
|
|
2168
|
+
email: "new@feltdb.dev".to_string(),
|
|
2169
|
+
age: 25,
|
|
2170
|
+
active: true,
|
|
2171
|
+
})
|
|
2172
|
+
.expect("insert");
|
|
2173
|
+
});
|
|
2174
|
+
|
|
2175
|
+
let updated = subscription.next().await;
|
|
2176
|
+
assert!(updated.is_some());
|
|
2177
|
+
let results = updated.unwrap();
|
|
2178
|
+
assert_eq!(results.len(), 2);
|
|
2179
|
+
let _ = fs::remove_file(path);
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
#[tokio::test]
|
|
2183
|
+
async fn live_query_filters_by_predicate() {
|
|
2184
|
+
let path = temp_file();
|
|
2185
|
+
let db = open(&path).expect("open");
|
|
2186
|
+
let users = db.capability("users");
|
|
2187
|
+
users
|
|
2188
|
+
.insert(User {
|
|
2189
|
+
email: "active@feltdb.dev".to_string(),
|
|
2190
|
+
age: 28,
|
|
2191
|
+
active: true,
|
|
2192
|
+
})
|
|
2193
|
+
.expect("insert");
|
|
2194
|
+
users
|
|
2195
|
+
.insert(User {
|
|
2196
|
+
email: "inactive@feltdb.dev".to_string(),
|
|
2197
|
+
age: 35,
|
|
2198
|
+
active: false,
|
|
2199
|
+
})
|
|
2200
|
+
.expect("insert");
|
|
2201
|
+
|
|
2202
|
+
let subscription = db
|
|
2203
|
+
.live_query::<User, _>(|u| u.active)
|
|
2204
|
+
.await
|
|
2205
|
+
.expect("live_query");
|
|
2206
|
+
let initial = subscription.initial_results();
|
|
2207
|
+
assert_eq!(initial.len(), 1);
|
|
2208
|
+
assert!(initial[0].active);
|
|
2209
|
+
let _ = fs::remove_file(path);
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
#[tokio::test]
|
|
2213
|
+
async fn change_events_are_emitted_on_insert() {
|
|
2214
|
+
let path = temp_file();
|
|
2215
|
+
let db = open(&path).expect("open");
|
|
2216
|
+
|
|
2217
|
+
let mut subscription = db
|
|
2218
|
+
.live_query::<User, _>(|_| true)
|
|
2219
|
+
.await
|
|
2220
|
+
.expect("live_query");
|
|
2221
|
+
let initial = subscription.initial_results();
|
|
2222
|
+
assert_eq!(initial.len(), 0);
|
|
2223
|
+
|
|
2224
|
+
let db_for_insert = db.clone();
|
|
2225
|
+
tokio::spawn(async move {
|
|
2226
|
+
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
|
2227
|
+
let users_inner = db_for_insert.capability("users");
|
|
2228
|
+
users_inner
|
|
2229
|
+
.insert(User {
|
|
2230
|
+
email: "event@feltdb.dev".to_string(),
|
|
2231
|
+
age: 30,
|
|
2232
|
+
active: true,
|
|
2233
|
+
})
|
|
2234
|
+
.expect("insert");
|
|
2235
|
+
});
|
|
2236
|
+
|
|
2237
|
+
let update = subscription.next().await;
|
|
2238
|
+
assert!(update.is_some());
|
|
2239
|
+
let users_result = update.unwrap();
|
|
2240
|
+
assert_eq!(users_result.len(), 1);
|
|
2241
|
+
let _ = fs::remove_file(path);
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
#[tokio::test]
|
|
2245
|
+
async fn capability_live_find_returns_initial_results() {
|
|
2246
|
+
let path = temp_file();
|
|
2247
|
+
let db = open(&path).expect("open");
|
|
2248
|
+
let users = db.capability("users");
|
|
2249
|
+
users
|
|
2250
|
+
.insert(User {
|
|
2251
|
+
email: "cap_live@feltdb.dev".to_string(),
|
|
2252
|
+
age: 28,
|
|
2253
|
+
active: true,
|
|
2254
|
+
})
|
|
2255
|
+
.expect("insert");
|
|
2256
|
+
users
|
|
2257
|
+
.insert(User {
|
|
2258
|
+
email: "cap_inactive@feltdb.dev".to_string(),
|
|
2259
|
+
age: 35,
|
|
2260
|
+
active: false,
|
|
2261
|
+
})
|
|
2262
|
+
.expect("insert");
|
|
2263
|
+
|
|
2264
|
+
let subscription = users
|
|
2265
|
+
.live_find::<User, _>(|u| u.active)
|
|
2266
|
+
.await
|
|
2267
|
+
.expect("live_find");
|
|
2268
|
+
let initial = subscription.initial_results();
|
|
2269
|
+
assert_eq!(initial.len(), 1);
|
|
2270
|
+
assert_eq!(initial[0].email, "cap_live@feltdb.dev");
|
|
2271
|
+
let _ = fs::remove_file(path);
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
#[tokio::test]
|
|
2275
|
+
async fn capability_live_find_streams_scoped_changes() {
|
|
2276
|
+
let path = temp_file();
|
|
2277
|
+
let db = open(&path).expect("open");
|
|
2278
|
+
let users = db.capability("users");
|
|
2279
|
+
let _orders = db.capability("orders");
|
|
2280
|
+
users
|
|
2281
|
+
.insert(User {
|
|
2282
|
+
email: "user_scoped@feltdb.dev".to_string(),
|
|
2283
|
+
age: 28,
|
|
2284
|
+
active: true,
|
|
2285
|
+
})
|
|
2286
|
+
.expect("insert");
|
|
2287
|
+
|
|
2288
|
+
let mut subscription = users
|
|
2289
|
+
.live_find::<User, _>(|u| u.active)
|
|
2290
|
+
.await
|
|
2291
|
+
.expect("live_find");
|
|
2292
|
+
let initial = subscription.initial_results();
|
|
2293
|
+
assert_eq!(initial.len(), 1);
|
|
2294
|
+
|
|
2295
|
+
let db_for_insert = db.clone();
|
|
2296
|
+
tokio::spawn(async move {
|
|
2297
|
+
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
|
2298
|
+
let users_inner = db_for_insert.capability("users");
|
|
2299
|
+
users_inner
|
|
2300
|
+
.insert(User {
|
|
2301
|
+
email: "new_user_scoped@feltdb.dev".to_string(),
|
|
2302
|
+
age: 25,
|
|
2303
|
+
active: true,
|
|
2304
|
+
})
|
|
2305
|
+
.expect("insert");
|
|
2306
|
+
});
|
|
2307
|
+
|
|
2308
|
+
let updated = subscription.next().await;
|
|
2309
|
+
assert!(updated.is_some());
|
|
2310
|
+
let results = updated.unwrap();
|
|
2311
|
+
assert_eq!(results.len(), 2);
|
|
2312
|
+
let _ = fs::remove_file(path);
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
#[test]
|
|
2316
|
+
fn vector_clocks_increment_on_mutations() {
|
|
2317
|
+
let path = temp_file();
|
|
2318
|
+
let db = open(&path).expect("open");
|
|
2319
|
+
|
|
2320
|
+
// Initial vector clock should be empty or have default values
|
|
2321
|
+
let initial_clock = db.get_vector_clock().expect("get_vector_clock");
|
|
2322
|
+
assert!(initial_clock.is_none() || initial_clock.as_ref().unwrap().is_empty());
|
|
2323
|
+
|
|
2324
|
+
// Insert first record
|
|
2325
|
+
db.insert(
|
|
2326
|
+
"users:1",
|
|
2327
|
+
User {
|
|
2328
|
+
email: "user1@feltdb.dev".to_string(),
|
|
2329
|
+
age: 25,
|
|
2330
|
+
active: true,
|
|
2331
|
+
},
|
|
2332
|
+
)
|
|
2333
|
+
.expect("insert");
|
|
2334
|
+
|
|
2335
|
+
// Check pending operations have vector clock
|
|
2336
|
+
let pending = db
|
|
2337
|
+
.get_pending_for_peer("peer-1", 0)
|
|
2338
|
+
.expect("get_pending_for_peer");
|
|
2339
|
+
assert_eq!(pending.len(), 1);
|
|
2340
|
+
assert!(pending[0].vector_clock.is_some());
|
|
2341
|
+
|
|
2342
|
+
// Insert second record
|
|
2343
|
+
db.insert(
|
|
2344
|
+
"users:2",
|
|
2345
|
+
User {
|
|
2346
|
+
email: "user2@feltdb.dev".to_string(),
|
|
2347
|
+
age: 30,
|
|
2348
|
+
active: false,
|
|
2349
|
+
},
|
|
2350
|
+
)
|
|
2351
|
+
.expect("insert");
|
|
2352
|
+
|
|
2353
|
+
// Check we have 2 operations with increasing sequence numbers
|
|
2354
|
+
let pending = db
|
|
2355
|
+
.get_pending_for_peer("peer-1", 0)
|
|
2356
|
+
.expect("get_pending_for_peer");
|
|
2357
|
+
assert_eq!(pending.len(), 2);
|
|
2358
|
+
assert_eq!(pending[0].sequence, 1);
|
|
2359
|
+
assert_eq!(pending[1].sequence, 2);
|
|
2360
|
+
|
|
2361
|
+
let _ = fs::remove_file(path);
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
#[test]
|
|
2365
|
+
fn update_creates_operation_with_vector_clock() {
|
|
2366
|
+
let path = temp_file();
|
|
2367
|
+
let db = open(&path).expect("open");
|
|
2368
|
+
|
|
2369
|
+
// Insert initial record
|
|
2370
|
+
db.insert(
|
|
2371
|
+
"users:1",
|
|
2372
|
+
User {
|
|
2373
|
+
email: "user1@feltdb.dev".to_string(),
|
|
2374
|
+
age: 25,
|
|
2375
|
+
active: true,
|
|
2376
|
+
},
|
|
2377
|
+
)
|
|
2378
|
+
.expect("insert");
|
|
2379
|
+
|
|
2380
|
+
// Update the record
|
|
2381
|
+
db.update(
|
|
2382
|
+
"users:1",
|
|
2383
|
+
User {
|
|
2384
|
+
email: "user1@feltdb.dev".to_string(),
|
|
2385
|
+
age: 26,
|
|
2386
|
+
active: true,
|
|
2387
|
+
},
|
|
2388
|
+
)
|
|
2389
|
+
.expect("update");
|
|
2390
|
+
|
|
2391
|
+
// Check we have operations for both insert and update
|
|
2392
|
+
let pending = db
|
|
2393
|
+
.get_pending_for_peer("peer-1", 0)
|
|
2394
|
+
.expect("get_pending_for_peer");
|
|
2395
|
+
assert_eq!(pending.len(), 2);
|
|
2396
|
+
assert_eq!(pending[0].op_type, OperationType::Insert);
|
|
2397
|
+
assert_eq!(pending[1].op_type, OperationType::Update);
|
|
2398
|
+
|
|
2399
|
+
// Both should have vector clocks
|
|
2400
|
+
assert!(pending[0].vector_clock.is_some());
|
|
2401
|
+
assert!(pending[1].vector_clock.is_some());
|
|
2402
|
+
|
|
2403
|
+
let _ = fs::remove_file(path);
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
#[test]
|
|
2407
|
+
fn delete_creates_operation_with_vector_clock() {
|
|
2408
|
+
let path = temp_file();
|
|
2409
|
+
let db = open(&path).expect("open");
|
|
2410
|
+
|
|
2411
|
+
// Insert record
|
|
2412
|
+
db.insert(
|
|
2413
|
+
"users:1",
|
|
2414
|
+
User {
|
|
2415
|
+
email: "user1@feltdb.dev".to_string(),
|
|
2416
|
+
age: 25,
|
|
2417
|
+
active: true,
|
|
2418
|
+
},
|
|
2419
|
+
)
|
|
2420
|
+
.expect("insert");
|
|
2421
|
+
|
|
2422
|
+
// Delete the record
|
|
2423
|
+
db.delete("users:1").expect("delete");
|
|
2424
|
+
|
|
2425
|
+
// Check we have operations for both insert and delete
|
|
2426
|
+
let pending = db
|
|
2427
|
+
.get_pending_for_peer("peer-1", 0)
|
|
2428
|
+
.expect("get_pending_for_peer");
|
|
2429
|
+
assert_eq!(pending.len(), 2);
|
|
2430
|
+
assert_eq!(pending[0].op_type, OperationType::Insert);
|
|
2431
|
+
assert_eq!(pending[1].op_type, OperationType::Delete);
|
|
2432
|
+
|
|
2433
|
+
// Both should have vector clocks
|
|
2434
|
+
assert!(pending[0].vector_clock.is_some());
|
|
2435
|
+
assert!(pending[1].vector_clock.is_some());
|
|
2436
|
+
|
|
2437
|
+
let _ = fs::remove_file(path);
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
#[test]
|
|
2441
|
+
fn apply_remote_operation_merges_vector_clocks() {
|
|
2442
|
+
let path = temp_file();
|
|
2443
|
+
let db = open(&path).expect("open");
|
|
2444
|
+
|
|
2445
|
+
// Create a remote operation with a vector clock
|
|
2446
|
+
let remote_op = Operation::insert(
|
|
2447
|
+
1,
|
|
2448
|
+
"remote-instance".to_string(),
|
|
2449
|
+
1,
|
|
2450
|
+
"users:100".to_string(),
|
|
2451
|
+
serde_json::json!({"email": "remote@feltdb.dev", "age": 30, "active": true}),
|
|
2452
|
+
"User".to_string(),
|
|
2453
|
+
"users".to_string(),
|
|
2454
|
+
)
|
|
2455
|
+
.with_vector_clock({
|
|
2456
|
+
let mut vc = std::collections::HashMap::new();
|
|
2457
|
+
vc.insert("remote-instance".to_string(), 1);
|
|
2458
|
+
vc
|
|
2459
|
+
});
|
|
2460
|
+
|
|
2461
|
+
// Apply the remote operation
|
|
2462
|
+
db.apply_remote_operation(remote_op)
|
|
2463
|
+
.expect("apply_remote_operation");
|
|
2464
|
+
|
|
2465
|
+
// Check the local vector clock was merged
|
|
2466
|
+
let clock = db.get_vector_clock().expect("get_vector_clock");
|
|
2467
|
+
assert!(clock.is_some());
|
|
2468
|
+
let clock = clock.unwrap();
|
|
2469
|
+
assert_eq!(clock.get("remote-instance"), Some(&1));
|
|
2470
|
+
|
|
2471
|
+
// Verify the data was applied
|
|
2472
|
+
let user: Option<User> = db.get("users:100").expect("get");
|
|
2473
|
+
assert!(user.is_some());
|
|
2474
|
+
|
|
2475
|
+
let _ = fs::remove_file(path);
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
#[test]
|
|
2479
|
+
fn operation_history_survives_restart_and_relays_transitively() {
|
|
2480
|
+
let source_path = temp_file();
|
|
2481
|
+
let relay_path = temp_file();
|
|
2482
|
+
let destination_path = temp_file();
|
|
2483
|
+
let source = open(&source_path).expect("open source");
|
|
2484
|
+
source
|
|
2485
|
+
.insert("docs:1", serde_json::json!({"title": "one"}))
|
|
2486
|
+
.unwrap();
|
|
2487
|
+
source
|
|
2488
|
+
.insert("docs:2", serde_json::json!({"title": "two"}))
|
|
2489
|
+
.unwrap();
|
|
2490
|
+
|
|
2491
|
+
let relay = open(&relay_path).expect("open relay");
|
|
2492
|
+
for operation in source.operations_since(&HashMap::new()).unwrap() {
|
|
2493
|
+
relay.apply_remote_operation(operation).unwrap();
|
|
2494
|
+
}
|
|
2495
|
+
drop(relay);
|
|
2496
|
+
|
|
2497
|
+
let relay = open(&relay_path).expect("reopen relay");
|
|
2498
|
+
let relayed = relay.operations_since(&HashMap::new()).unwrap();
|
|
2499
|
+
assert_eq!(relayed.len(), 2);
|
|
2500
|
+
let destination = open(&destination_path).expect("open destination");
|
|
2501
|
+
for operation in relayed {
|
|
2502
|
+
destination.apply_remote_operation(operation).unwrap();
|
|
2503
|
+
}
|
|
2504
|
+
assert_eq!(
|
|
2505
|
+
destination.get_value("docs:2").unwrap(),
|
|
2506
|
+
Some(serde_json::json!({"title": "two"}))
|
|
2507
|
+
);
|
|
2508
|
+
|
|
2509
|
+
let _ = fs::remove_file(source_path);
|
|
2510
|
+
let _ = fs::remove_file(relay_path);
|
|
2511
|
+
let _ = fs::remove_file(destination_path);
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
#[test]
|
|
2515
|
+
fn remote_operation_sequence_gaps_are_rejected() {
|
|
2516
|
+
let path = temp_file();
|
|
2517
|
+
let db = open(&path).unwrap();
|
|
2518
|
+
let operation = Operation::insert(
|
|
2519
|
+
2,
|
|
2520
|
+
"remote".to_string(),
|
|
2521
|
+
2,
|
|
2522
|
+
"docs:2".to_string(),
|
|
2523
|
+
serde_json::json!({"title": "two"}),
|
|
2524
|
+
"Value".to_string(),
|
|
2525
|
+
"docs".to_string(),
|
|
2526
|
+
);
|
|
2527
|
+
assert!(db.apply_remote_operation(operation).is_err());
|
|
2528
|
+
assert!(db.get_value("docs:2").unwrap().is_none());
|
|
2529
|
+
let _ = fs::remove_file(path);
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
#[test]
|
|
2533
|
+
fn peer_version_acknowledgements_survive_restart() {
|
|
2534
|
+
let path = temp_file();
|
|
2535
|
+
let db = open(&path).unwrap();
|
|
2536
|
+
db.insert("docs:1", serde_json::json!({"title": "one"}))
|
|
2537
|
+
.unwrap();
|
|
2538
|
+
let versions = db.operation_versions().unwrap();
|
|
2539
|
+
db.acknowledge_peer_versions("peer-a".to_string(), versions.clone())
|
|
2540
|
+
.unwrap();
|
|
2541
|
+
drop(db);
|
|
2542
|
+
|
|
2543
|
+
let reopened = open(&path).unwrap();
|
|
2544
|
+
assert_eq!(
|
|
2545
|
+
reopened.acknowledged_peer_versions("peer-a").unwrap(),
|
|
2546
|
+
versions
|
|
2547
|
+
);
|
|
2548
|
+
assert_eq!(
|
|
2549
|
+
reopened
|
|
2550
|
+
.acknowledged_operation_count(&["peer-a".to_string()])
|
|
2551
|
+
.unwrap(),
|
|
2552
|
+
1
|
|
2553
|
+
);
|
|
2554
|
+
let _ = fs::remove_file(sync_metadata_path(&path));
|
|
2555
|
+
let _ = fs::remove_file(path);
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
#[test]
|
|
2559
|
+
fn compacted_history_bootstraps_a_new_peer_without_sequence_gaps() {
|
|
2560
|
+
let source_path = temp_file();
|
|
2561
|
+
let destination_path = temp_file();
|
|
2562
|
+
let source = open(&source_path).unwrap();
|
|
2563
|
+
source
|
|
2564
|
+
.insert("docs:1", serde_json::json!({"title": "one"}))
|
|
2565
|
+
.unwrap();
|
|
2566
|
+
source
|
|
2567
|
+
.insert("docs:2", serde_json::json!({"title": "two"}))
|
|
2568
|
+
.unwrap();
|
|
2569
|
+
let versions = source.operation_versions().unwrap();
|
|
2570
|
+
source
|
|
2571
|
+
.acknowledge_peer_versions("peer-a".to_string(), versions.clone())
|
|
2572
|
+
.unwrap();
|
|
2573
|
+
assert_eq!(
|
|
2574
|
+
source
|
|
2575
|
+
.compact_operation_log(&["peer-a".to_string()])
|
|
2576
|
+
.unwrap(),
|
|
2577
|
+
2
|
|
2578
|
+
);
|
|
2579
|
+
assert!(source.operations_since(&HashMap::new()).unwrap().is_empty());
|
|
2580
|
+
drop(source);
|
|
2581
|
+
|
|
2582
|
+
let source = open(&source_path).unwrap();
|
|
2583
|
+
assert_eq!(source.operation_versions().unwrap(), versions);
|
|
2584
|
+
let snapshot = source.export_snapshot().unwrap();
|
|
2585
|
+
let destination = open(&destination_path).unwrap();
|
|
2586
|
+
destination.install_snapshot(snapshot).unwrap();
|
|
2587
|
+
assert_eq!(destination.operation_versions().unwrap(), versions);
|
|
2588
|
+
assert_eq!(
|
|
2589
|
+
destination.get_value("docs:2").unwrap(),
|
|
2590
|
+
Some(serde_json::json!({"title": "two"}))
|
|
2591
|
+
);
|
|
2592
|
+
|
|
2593
|
+
source
|
|
2594
|
+
.insert("docs:3", serde_json::json!({"title": "three"}))
|
|
2595
|
+
.unwrap();
|
|
2596
|
+
let next = source.operations_since(&versions).unwrap();
|
|
2597
|
+
assert_eq!(next.len(), 1);
|
|
2598
|
+
destination.apply_remote_operation(next[0].clone()).unwrap();
|
|
2599
|
+
assert!(destination.get_value("docs:3").unwrap().is_some());
|
|
2600
|
+
|
|
2601
|
+
let _ = fs::remove_file(sync_metadata_path(&source_path));
|
|
2602
|
+
let _ = fs::remove_file(sync_metadata_path(&destination_path));
|
|
2603
|
+
let _ = fs::remove_file(source_path);
|
|
2604
|
+
let _ = fs::remove_file(destination_path);
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
#[test]
|
|
2608
|
+
fn maintained_cardinality_unfiltered_count_basic() {
|
|
2609
|
+
let path = temp_file();
|
|
2610
|
+
let db = open(&path).unwrap();
|
|
2611
|
+
let capability = "test_collection";
|
|
2612
|
+
|
|
2613
|
+
let mutations = vec![
|
|
2614
|
+
AtomicMutation {
|
|
2615
|
+
capability: capability.to_string(),
|
|
2616
|
+
key: "rec-1".to_string(),
|
|
2617
|
+
value: Some(serde_json::json!({"id": 1, "name": "Record 1"})),
|
|
2618
|
+
},
|
|
2619
|
+
AtomicMutation {
|
|
2620
|
+
capability: capability.to_string(),
|
|
2621
|
+
key: "rec-2".to_string(),
|
|
2622
|
+
value: Some(serde_json::json!({"id": 2, "name": "Record 2"})),
|
|
2623
|
+
},
|
|
2624
|
+
];
|
|
2625
|
+
|
|
2626
|
+
db.apply_atomic_transaction("txn-1", None, &[], &mutations, None).unwrap();
|
|
2627
|
+
|
|
2628
|
+
let cardinality = db.collection_cardinality(capability).unwrap();
|
|
2629
|
+
assert_eq!(cardinality, 2, "Cardinality should be 2 after 2 inserts");
|
|
2630
|
+
|
|
2631
|
+
let _ = fs::remove_file(path);
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
#[test]
|
|
2635
|
+
fn maintained_cardinality_insert_increments() {
|
|
2636
|
+
let path = temp_file();
|
|
2637
|
+
let db = open(&path).unwrap();
|
|
2638
|
+
let capability = "test_collection";
|
|
2639
|
+
|
|
2640
|
+
for i in 1..=5 {
|
|
2641
|
+
let mutations = vec![AtomicMutation {
|
|
2642
|
+
capability: capability.to_string(),
|
|
2643
|
+
key: format!("rec-{}", i),
|
|
2644
|
+
value: Some(serde_json::json!({"id": i, "name": format!("Record {}", i)})),
|
|
2645
|
+
}];
|
|
2646
|
+
|
|
2647
|
+
db.apply_atomic_transaction(&format!("txn-{}", i), None, &[], &mutations, None).unwrap();
|
|
2648
|
+
let cardinality = db.collection_cardinality(capability).unwrap();
|
|
2649
|
+
assert_eq!(cardinality, i as u64, "Cardinality should be {} after {} inserts", i, i);
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
let _ = fs::remove_file(path);
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
#[test]
|
|
2656
|
+
fn maintained_cardinality_delete_decrements() {
|
|
2657
|
+
let path = temp_file();
|
|
2658
|
+
let db = open(&path).unwrap();
|
|
2659
|
+
let capability = "test_collection";
|
|
2660
|
+
|
|
2661
|
+
let insert_mutations = vec![
|
|
2662
|
+
AtomicMutation {
|
|
2663
|
+
capability: capability.to_string(),
|
|
2664
|
+
key: "rec-1".to_string(),
|
|
2665
|
+
value: Some(serde_json::json!({"id": 1})),
|
|
2666
|
+
},
|
|
2667
|
+
AtomicMutation {
|
|
2668
|
+
capability: capability.to_string(),
|
|
2669
|
+
key: "rec-2".to_string(),
|
|
2670
|
+
value: Some(serde_json::json!({"id": 2})),
|
|
2671
|
+
},
|
|
2672
|
+
AtomicMutation {
|
|
2673
|
+
capability: capability.to_string(),
|
|
2674
|
+
key: "rec-3".to_string(),
|
|
2675
|
+
value: Some(serde_json::json!({"id": 3})),
|
|
2676
|
+
},
|
|
2677
|
+
];
|
|
2678
|
+
|
|
2679
|
+
db.apply_atomic_transaction("txn-insert", None, &[], &insert_mutations, None).unwrap();
|
|
2680
|
+
assert_eq!(db.collection_cardinality(capability).unwrap(), 3);
|
|
2681
|
+
|
|
2682
|
+
let delete_mutations = vec![
|
|
2683
|
+
AtomicMutation {
|
|
2684
|
+
capability: capability.to_string(),
|
|
2685
|
+
key: "rec-1".to_string(),
|
|
2686
|
+
value: None,
|
|
2687
|
+
},
|
|
2688
|
+
];
|
|
2689
|
+
|
|
2690
|
+
db.apply_atomic_transaction("txn-delete", None, &[], &delete_mutations, None).unwrap();
|
|
2691
|
+
assert_eq!(db.collection_cardinality(capability).unwrap(), 2, "Cardinality should be 2 after deleting 1 record");
|
|
2692
|
+
|
|
2693
|
+
let _ = fs::remove_file(path);
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
#[test]
|
|
2697
|
+
fn maintained_cardinality_update_does_not_change_count() {
|
|
2698
|
+
let path = temp_file();
|
|
2699
|
+
let db = open(&path).unwrap();
|
|
2700
|
+
let capability = "test_collection";
|
|
2701
|
+
|
|
2702
|
+
let insert_mutations = vec![AtomicMutation {
|
|
2703
|
+
capability: capability.to_string(),
|
|
2704
|
+
key: "rec-1".to_string(),
|
|
2705
|
+
value: Some(serde_json::json!({"id": 1, "status": "active"})),
|
|
2706
|
+
}];
|
|
2707
|
+
|
|
2708
|
+
db.apply_atomic_transaction("txn-insert", None, &[], &insert_mutations, None).unwrap();
|
|
2709
|
+
assert_eq!(db.collection_cardinality(capability).unwrap(), 1);
|
|
2710
|
+
|
|
2711
|
+
let update_mutations = vec![AtomicMutation {
|
|
2712
|
+
capability: capability.to_string(),
|
|
2713
|
+
key: "rec-1".to_string(),
|
|
2714
|
+
value: Some(serde_json::json!({"id": 1, "status": "inactive"})),
|
|
2715
|
+
}];
|
|
2716
|
+
|
|
2717
|
+
db.apply_atomic_transaction("txn-update", None, &[], &update_mutations, None).unwrap();
|
|
2718
|
+
assert_eq!(db.collection_cardinality(capability).unwrap(), 1, "Cardinality should remain 1 after update");
|
|
2719
|
+
|
|
2720
|
+
let _ = fs::remove_file(path);
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
#[test]
|
|
2724
|
+
fn maintained_cardinality_survives_restart() {
|
|
2725
|
+
let path = temp_file();
|
|
2726
|
+
|
|
2727
|
+
{
|
|
2728
|
+
let db = open(&path).unwrap();
|
|
2729
|
+
let capability = "test_collection";
|
|
2730
|
+
|
|
2731
|
+
let mutations = vec![
|
|
2732
|
+
AtomicMutation {
|
|
2733
|
+
capability: capability.to_string(),
|
|
2734
|
+
key: "rec-1".to_string(),
|
|
2735
|
+
value: Some(serde_json::json!({"id": 1})),
|
|
2736
|
+
},
|
|
2737
|
+
AtomicMutation {
|
|
2738
|
+
capability: capability.to_string(),
|
|
2739
|
+
key: "rec-2".to_string(),
|
|
2740
|
+
value: Some(serde_json::json!({"id": 2})),
|
|
2741
|
+
},
|
|
2742
|
+
];
|
|
2743
|
+
|
|
2744
|
+
db.apply_atomic_transaction("txn-1", None, &[], &mutations, None).unwrap();
|
|
2745
|
+
assert_eq!(db.collection_cardinality(capability).unwrap(), 2);
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
let reopened = open(&path).unwrap();
|
|
2749
|
+
let capability = "test_collection";
|
|
2750
|
+
let cardinality = reopened.collection_cardinality(capability).unwrap();
|
|
2751
|
+
assert_eq!(cardinality, 2, "Cardinality should survive restart and be recomputed from rows");
|
|
2752
|
+
|
|
2753
|
+
let _ = fs::remove_file(path);
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
#[test]
|
|
2757
|
+
fn maintained_cardinality_empty_collection() {
|
|
2758
|
+
let path = temp_file();
|
|
2759
|
+
let db = open(&path).unwrap();
|
|
2760
|
+
let capability = "empty_collection";
|
|
2761
|
+
|
|
2762
|
+
let cardinality = db.collection_cardinality(capability).unwrap();
|
|
2763
|
+
assert_eq!(cardinality, 0, "Empty collection should have cardinality 0");
|
|
2764
|
+
|
|
2765
|
+
let _ = fs::remove_file(path);
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
#[test]
|
|
2769
|
+
fn maintained_cardinality_large_collection() {
|
|
2770
|
+
let path = temp_file();
|
|
2771
|
+
let db = open(&path).unwrap();
|
|
2772
|
+
let capability = "large_collection";
|
|
2773
|
+
|
|
2774
|
+
let mut mutations = Vec::new();
|
|
2775
|
+
for i in 1..=100 {
|
|
2776
|
+
mutations.push(AtomicMutation {
|
|
2777
|
+
capability: capability.to_string(),
|
|
2778
|
+
key: format!("rec-{}", i),
|
|
2779
|
+
value: Some(serde_json::json!({"id": i, "name": format!("Record {}", i)})),
|
|
2780
|
+
});
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
db.apply_atomic_transaction("txn-large", None, &[], &mutations, None).unwrap();
|
|
2784
|
+
let cardinality = db.collection_cardinality(capability).unwrap();
|
|
2785
|
+
assert_eq!(cardinality, 100, "Large collection with 100 records should have cardinality 100");
|
|
2786
|
+
|
|
2787
|
+
let _ = fs::remove_file(path);
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
#[test]
|
|
2791
|
+
fn maintained_cardinality_multiple_collections_isolated() {
|
|
2792
|
+
let path = temp_file();
|
|
2793
|
+
let db = open(&path).unwrap();
|
|
2794
|
+
|
|
2795
|
+
let col1 = "collection_1";
|
|
2796
|
+
let col2 = "collection_2";
|
|
2797
|
+
|
|
2798
|
+
let mutations1 = vec![
|
|
2799
|
+
AtomicMutation {
|
|
2800
|
+
capability: col1.to_string(),
|
|
2801
|
+
key: "rec-1".to_string(),
|
|
2802
|
+
value: Some(serde_json::json!({"id": 1})),
|
|
2803
|
+
},
|
|
2804
|
+
AtomicMutation {
|
|
2805
|
+
capability: col1.to_string(),
|
|
2806
|
+
key: "rec-2".to_string(),
|
|
2807
|
+
value: Some(serde_json::json!({"id": 2})),
|
|
2808
|
+
},
|
|
2809
|
+
];
|
|
2810
|
+
|
|
2811
|
+
db.apply_atomic_transaction("txn-col1", None, &[], &mutations1, None).unwrap();
|
|
2812
|
+
|
|
2813
|
+
let mutations2 = vec![
|
|
2814
|
+
AtomicMutation {
|
|
2815
|
+
capability: col2.to_string(),
|
|
2816
|
+
key: "rec-1".to_string(),
|
|
2817
|
+
value: Some(serde_json::json!({"id": 1})),
|
|
2818
|
+
},
|
|
2819
|
+
AtomicMutation {
|
|
2820
|
+
capability: col2.to_string(),
|
|
2821
|
+
key: "rec-2".to_string(),
|
|
2822
|
+
value: Some(serde_json::json!({"id": 2})),
|
|
2823
|
+
},
|
|
2824
|
+
AtomicMutation {
|
|
2825
|
+
capability: col2.to_string(),
|
|
2826
|
+
key: "rec-3".to_string(),
|
|
2827
|
+
value: Some(serde_json::json!({"id": 3})),
|
|
2828
|
+
},
|
|
2829
|
+
];
|
|
2830
|
+
|
|
2831
|
+
db.apply_atomic_transaction("txn-col2", None, &[], &mutations2, None).unwrap();
|
|
2832
|
+
|
|
2833
|
+
assert_eq!(db.collection_cardinality(col1).unwrap(), 2);
|
|
2834
|
+
assert_eq!(db.collection_cardinality(col2).unwrap(), 3);
|
|
2835
|
+
|
|
2836
|
+
let _ = fs::remove_file(path);
|
|
2837
|
+
}
|
|
2838
|
+
}
|