@feltdb/core 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/dist/agent-state.d.ts +20 -0
  2. package/dist/agent-state.d.ts.map +1 -1
  3. package/dist/agent-state.js +36 -0
  4. package/dist/build-identity.json +12 -4
  5. package/dist/cli/commands.js +187 -4
  6. package/dist/cli/publish-engine.js +116 -16
  7. package/dist/collection.d.ts.map +1 -1
  8. package/dist/collection.js +3 -0
  9. package/dist/create/build-identity.json +12 -4
  10. package/dist/create/managed-account.js +93 -35
  11. package/dist/create/package-versions.js +1 -1
  12. package/dist/create/server-source/Cargo.lock +6 -509
  13. package/dist/create/server-source/Cargo.toml +2 -13
  14. package/dist/create/server-source/crates/feltdb-client/Cargo.toml +1 -1
  15. package/dist/create/server-source/crates/feltdb-server/Cargo.toml +1 -1
  16. package/dist/create/server-source/crates/feltdb-server/src/main.rs +255 -12
  17. package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +1 -1
  18. package/dist/db.d.ts +11 -1
  19. package/dist/db.d.ts.map +1 -1
  20. package/dist/db.js +15 -2
  21. package/dist/error-codes.d.ts +5 -0
  22. package/dist/error-codes.d.ts.map +1 -1
  23. package/dist/error-codes.js +9 -1
  24. package/dist/http-db.d.ts +16 -0
  25. package/dist/http-db.d.ts.map +1 -1
  26. package/dist/http-db.js +36 -6
  27. package/dist/managed-recovery.d.ts +6 -0
  28. package/dist/managed-recovery.d.ts.map +1 -1
  29. package/dist/managed-recovery.js +9 -0
  30. package/dist/studio-app/assets/{feltdb_wasm-BjM1ylL3.js → feltdb_wasm-DtybUY0q.js} +1 -1
  31. package/dist/studio-app/assets/feltdb_wasm_bg-DsgIUwaB.wasm +0 -0
  32. package/dist/studio-app/assets/index-C4OffsZl.js +37 -0
  33. package/dist/studio-app/index.html +1 -1
  34. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  35. package/dist/wasm/package.json +1 -1
  36. package/dist/workload.d.ts +55 -3
  37. package/dist/workload.d.ts.map +1 -1
  38. package/dist/workload.js +23 -0
  39. package/dist/workspace/workspace-connection.d.ts.map +1 -1
  40. package/dist/workspace/workspace-connection.js +14 -3
  41. package/package.json +1 -1
  42. package/dist/create/server-source/crates/feltdb/Cargo.lock +0 -175
  43. package/dist/create/server-source/crates/feltdb/Cargo.toml +0 -102
  44. package/dist/create/server-source/crates/feltdb/LICENSE +0 -21
  45. package/dist/create/server-source/crates/feltdb/README.md +0 -75
  46. package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +0 -370
  47. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +0 -589
  48. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +0 -247
  49. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +0 -440
  50. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +0 -275
  51. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +0 -381
  52. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +0 -467
  53. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +0 -489
  54. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +0 -281
  55. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +0 -606
  56. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +0 -531
  57. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +0 -462
  58. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +0 -505
  59. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +0 -484
  60. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +0 -758
  61. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +0 -421
  62. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +0 -329
  63. package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +0 -1705
  64. package/dist/create/server-source/crates/feltdb/src/acquisition.rs +0 -286
  65. package/dist/create/server-source/crates/feltdb/src/admission.rs +0 -185
  66. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +0 -507
  67. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +0 -539
  68. package/dist/create/server-source/crates/feltdb/src/agent_state.rs +0 -320
  69. package/dist/create/server-source/crates/feltdb/src/agent_transition_contract.rs +0 -1545
  70. package/dist/create/server-source/crates/feltdb/src/analytics.rs +0 -521
  71. package/dist/create/server-source/crates/feltdb/src/application.rs +0 -2674
  72. package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +0 -1070
  73. package/dist/create/server-source/crates/feltdb/src/authority.rs +0 -479
  74. package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +0 -847
  75. package/dist/create/server-source/crates/feltdb/src/authorization.rs +0 -1030
  76. package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +0 -1122
  77. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority-client.rs +0 -32
  78. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority.rs +0 -19
  79. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +0 -1474
  80. package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +0 -8
  81. package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +0 -418
  82. package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +0 -482
  83. package/dist/create/server-source/crates/feltdb/src/capability.rs +0 -903
  84. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +0 -263
  85. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -77
  86. package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +0 -496
  87. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +0 -2574
  88. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +0 -212
  89. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +0 -422
  90. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +0 -445
  91. package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +0 -465
  92. package/dist/create/server-source/crates/feltdb/src/convergence.rs +0 -634
  93. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +0 -427
  94. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +0 -376
  95. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +0 -365
  96. package/dist/create/server-source/crates/feltdb/src/cron.rs +0 -294
  97. package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +0 -483
  98. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +0 -477
  99. package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +0 -1003
  100. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +0 -1334
  101. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +0 -368
  102. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +0 -231
  103. package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +0 -482
  104. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +0 -203
  105. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +0 -317
  106. package/dist/create/server-source/crates/feltdb/src/equality_index.rs +0 -595
  107. package/dist/create/server-source/crates/feltdb/src/execution.rs +0 -426
  108. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +0 -214
  109. package/dist/create/server-source/crates/feltdb/src/indexing.rs +0 -776
  110. package/dist/create/server-source/crates/feltdb/src/lib.rs +0 -7365
  111. package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +0 -234
  112. package/dist/create/server-source/crates/feltdb/src/materialization.rs +0 -184
  113. package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -266
  114. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +0 -310
  115. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +0 -736
  116. package/dist/create/server-source/crates/feltdb/src/observability.rs +0 -379
  117. package/dist/create/server-source/crates/feltdb/src/operation.rs +0 -290
  118. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +0 -439
  119. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +0 -349
  120. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +0 -332
  121. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +0 -718
  122. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +0 -487
  123. package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +0 -166
  124. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +0 -289
  125. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +0 -566
  126. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +0 -3391
  127. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +0 -203
  128. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +0 -261
  129. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +0 -502
  130. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +0 -216
  131. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +0 -303
  132. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +0 -323
  133. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +0 -531
  134. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +0 -406
  135. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +0 -436
  136. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +0 -387
  137. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +0 -378
  138. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +0 -359
  139. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +0 -372
  140. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +0 -404
  141. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +0 -376
  142. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +0 -461
  143. package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +0 -2125
  144. package/dist/create/server-source/crates/feltdb/src/production_api.rs +0 -418
  145. package/dist/create/server-source/crates/feltdb/src/provenance.rs +0 -207
  146. package/dist/create/server-source/crates/feltdb/src/query_execution_diagnostics.rs +0 -126
  147. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +0 -218
  148. package/dist/create/server-source/crates/feltdb/src/read_basis.rs +0 -257
  149. package/dist/create/server-source/crates/feltdb/src/references.rs +0 -404
  150. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +0 -401
  151. package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +0 -501
  152. package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +0 -680
  153. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +0 -163
  154. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +0 -133
  155. package/dist/create/server-source/crates/feltdb/src/routing.rs +0 -409
  156. package/dist/create/server-source/crates/feltdb/src/semantic_decision.rs +0 -2029
  157. package/dist/create/server-source/crates/feltdb/src/semantic_query.rs +0 -599
  158. package/dist/create/server-source/crates/feltdb/src/sharding.rs +0 -549
  159. package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +0 -516
  160. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +0 -6003
  161. package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +0 -222
  162. package/dist/create/server-source/crates/feltdb/src/state_facade.rs +0 -320
  163. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +0 -291
  164. package/dist/create/server-source/crates/feltdb/src/state_model.rs +0 -4158
  165. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +0 -379
  166. package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +0 -673
  167. package/dist/create/server-source/crates/feltdb/src/storage.rs +0 -273
  168. package/dist/create/server-source/crates/feltdb/src/submission.rs +0 -471
  169. package/dist/create/server-source/crates/feltdb/src/sync.rs +0 -495
  170. package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +0 -822
  171. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +0 -433
  172. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +0 -462
  173. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +0 -965
  174. package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +0 -1120
  175. package/dist/create/server-source/crates/feltdb/src/transactions.rs +0 -643
  176. package/dist/create/server-source/crates/feltdb/src/trigger.rs +0 -390
  177. package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +0 -749
  178. package/dist/create/server-source/crates/feltdb/src/versioned_object_substrate.rs +0 -708
  179. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +0 -1269
  180. package/dist/create/server-source/crates/feltdb/src/workflow.rs +0 -713
  181. package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +0 -510
  182. package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +0 -354
  183. package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +0 -594
  184. package/dist/create/server-source/crates/feltdb/src/workload.rs +0 -4222
  185. package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +0 -456
  186. package/dist/create/server-source/crates/feltdb/tests/agent_state_native.rs +0 -1188
  187. package/dist/create/server-source/crates/feltdb/tests/agent_transition_contract_conformance.rs +0 -2884
  188. package/dist/create/server-source/crates/feltdb/tests/authority_process.rs +0 -297
  189. package/dist/create/server-source/crates/feltdb/tests/authority_transaction_identity.rs +0 -534
  190. package/dist/create/server-source/crates/feltdb/tests/bounded_read_contract.rs +0 -132
  191. package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +0 -299
  192. package/dist/create/server-source/crates/feltdb/tests/common/mod.rs +0 -96
  193. package/dist/create/server-source/crates/feltdb/tests/compaction_stall_contract.rs +0 -272
  194. package/dist/create/server-source/crates/feltdb/tests/crash_durability_contract.rs +0 -467
  195. package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +0 -309
  196. package/dist/create/server-source/crates/feltdb/tests/durable_backup_contract.rs +0 -445
  197. package/dist/create/server-source/crates/feltdb/tests/durable_corruption_contract.rs +0 -518
  198. package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +0 -392
  199. package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +0 -850
  200. package/dist/create/server-source/crates/feltdb/tests/fixtures/agent_transition_corpus.json +0 -23340
  201. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +0 -1916
  202. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +0 -1878
  203. package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +0 -1862
  204. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_evidence.rs +0 -123
  205. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_substrate_completion.rs +0 -299
  206. package/dist/create/server-source/crates/feltdb/tests/local_process_ownership.rs +0 -69
  207. package/dist/create/server-source/crates/feltdb/tests/managed_incident_regression.rs +0 -191
  208. package/dist/create/server-source/crates/feltdb/tests/operational_health_contract.rs +0 -278
  209. package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +0 -234
  210. package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +0 -947
  211. package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +0 -404
  212. package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +0 -916
  213. package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +0 -1045
  214. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +0 -347
  215. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +0 -361
  216. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +0 -330
  217. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +0 -93
  218. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +0 -622
  219. package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +0 -517
  220. package/dist/create/server-source/crates/feltdb/tests/production_certification.rs +0 -1153
  221. package/dist/create/server-source/crates/feltdb/tests/production_contract.rs +0 -771
  222. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +0 -1383
  223. package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +0 -868
  224. package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +0 -221
  225. package/dist/create/server-source/crates/feltdb/tests/replicated_history_contract.rs +0 -417
  226. package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +0 -338
  227. package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +0 -1039
  228. package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +0 -739
  229. package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +0 -452
  230. package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +0 -81
  231. package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +0 -438
  232. package/dist/create/server-source/crates/feltdb/tests/semantic_decision.rs +0 -1165
  233. package/dist/create/server-source/crates/feltdb/tests/semantic_query.rs +0 -767
  234. package/dist/create/server-source/crates/feltdb/tests/semantic_query_contract.rs +0 -363
  235. package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +0 -1799
  236. package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +0 -1316
  237. package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +0 -358
  238. package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +0 -365
  239. package/dist/create/server-source/crates/feltdb/tests/state_ref_authority.rs +0 -918
  240. package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +0 -299
  241. package/dist/create/server-source/crates/feltdb/tests/storage_independent_versioned_object_substrate.rs +0 -328
  242. package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +0 -255
  243. package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +0 -249
  244. package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +0 -994
  245. package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +0 -484
  246. package/dist/studio-app/assets/feltdb_wasm_bg-TseJn2Ax.wasm +0 -0
  247. package/dist/studio-app/assets/index-1wd0uKua.js +0 -30
@@ -1,1120 +0,0 @@
1
- //! Transaction-level preconditions: version, epoch and lease.
2
- //!
3
- //! The primitive these tests hold to its contract lets a caller do the thing a
4
- //! shared authority otherwise makes impossible:
5
- //!
6
- //! ```text
7
- //! read state -> decide -> conditionally commit several records
8
- //! ```
9
- //!
10
- //! with the condition evaluated by the authority inside the same atomic
11
- //! boundary as the writes. Client-side checking cannot provide this: between a
12
- //! client's check and its write, another writer commits.
13
- //!
14
- //! The invariant, which every test here is an attempt to break:
15
- //!
16
- //! ```text
17
- //! all preconditions pass -> all writes commit
18
- //! any precondition fails -> ZERO writes commit
19
- //! ```
20
- //!
21
- //! There is no third outcome, and in particular no state in which the guarded
22
- //! record is checked while the other staged records commit anyway.
23
- //!
24
- //! ## What "version" means here
25
- //!
26
- //! The document's `__version` -- the field a caller reads back and passes to
27
- //! `updateIfVersion` -- not the row's internal storage sequence. Those are two
28
- //! different numbers that happen to share a name, and fencing on the wrong one
29
- //! would compare a caller's value against something the caller never sees.
30
- //!
31
- //! The authority owns conditional transaction versions: `requireAbsent`
32
- //! creates version 1 and a version-fenced write of N stores N + 1. Both happen
33
- //! inside the same lock as validation. Unconditional writes remain verbatim.
34
-
35
- #[cfg(test)]
36
- mod tests {
37
- use crate::{AtomicMutation, FeltDb, FlowError, PreconditionFailure, RecordPrecondition};
38
- use serde_json::json;
39
- use tempfile::TempDir;
40
-
41
- fn db(dir: &TempDir) -> FeltDb {
42
- FeltDb::open(dir.path().join("app.log")).expect("open")
43
- }
44
-
45
- /// Storage keys are namespaced `collection:id`; the capability is the
46
- /// collection. Every helper below takes the bare id and namespaces it, so
47
- /// the tests read the way a caller thinks.
48
- fn key(collection: &str, id: &str) -> String {
49
- format!("{collection}:{id}")
50
- }
51
-
52
- fn set(collection: &str, id: &str, value: serde_json::Value) -> AtomicMutation {
53
- AtomicMutation {
54
- capability: collection.to_string(),
55
- key: key(collection, id),
56
- value: Some(value),
57
- }
58
- }
59
-
60
- fn delete(collection: &str, id: &str) -> AtomicMutation {
61
- AtomicMutation {
62
- capability: collection.to_string(),
63
- key: key(collection, id),
64
- value: None,
65
- }
66
- }
67
-
68
- fn precondition(collection: &str, id: &str) -> RecordPrecondition {
69
- RecordPrecondition {
70
- capability: collection.to_string(),
71
- key: key(collection, id),
72
- ..Default::default()
73
- }
74
- }
75
-
76
- fn expect_version(collection: &str, id: &str, version: u64) -> RecordPrecondition {
77
- RecordPrecondition {
78
- expected_version: Some(version),
79
- ..precondition(collection, id)
80
- }
81
- }
82
-
83
- fn present(db: &FeltDb, collection: &str, id: &str) -> bool {
84
- db.get_value(&key(collection, id)).expect("read").is_some()
85
- }
86
-
87
- fn field(db: &FeltDb, collection: &str, id: &str, name: &str) -> serde_json::Value {
88
- db.get_value(&key(collection, id))
89
- .expect("read")
90
- .and_then(|value| value.get(name).cloned())
91
- .unwrap_or(serde_json::Value::Null)
92
- }
93
-
94
- fn version_of(db: &FeltDb, collection: &str, id: &str) -> u64 {
95
- field(db, collection, id, "__version").as_u64().unwrap_or(0)
96
- }
97
-
98
- /// Seed a record at a chosen version, as an application would hold it.
99
- fn seed(db: &FeltDb, collection: &str, id: &str, version: u64, extra: serde_json::Value) {
100
- let mut value = json!({ "id": id, "__version": version });
101
- if let Some(fields) = extra.as_object() {
102
- for (name, field) in fields {
103
- value
104
- .as_object_mut()
105
- .unwrap()
106
- .insert(name.clone(), field.clone());
107
- }
108
- }
109
- db.apply_atomic_transaction(
110
- &format!("seed-{collection}-{id}-{version}"),
111
- None,
112
- &[],
113
- &[set(collection, id, value)],
114
- None,
115
- )
116
- .expect("seed commits");
117
- }
118
-
119
- fn guarded(
120
- db: &FeltDb,
121
- id: &str,
122
- preconditions: &[RecordPrecondition],
123
- mutations: &[AtomicMutation],
124
- ) -> Result<crate::AtomicCommit, FlowError> {
125
- db.apply_atomic_transaction_guarded(id, None, None, &[], preconditions, mutations, None)
126
- }
127
-
128
- fn failure(error: FlowError) -> PreconditionFailure {
129
- match error {
130
- FlowError::PreconditionFailed(failure) => *failure,
131
- other => panic!("expected a structured precondition failure, got {other:?}"),
132
- }
133
- }
134
-
135
- // -- the acceptance invariant -----------------------------------------
136
-
137
- #[test]
138
- fn a_satisfied_precondition_commits_every_staged_write() {
139
- let dir = TempDir::new().unwrap();
140
- let db = db(&dir);
141
- seed(&db, "sessions", "s1", 7, json!({}));
142
- seed(&db, "outbox", "o1", 1, json!({}));
143
-
144
- let commit = guarded(
145
- &db,
146
- "tx-ok",
147
- &[expect_version("sessions", "s1", 7)],
148
- &[
149
- set(
150
- "sessions",
151
- "s1",
152
- json!({ "id": "s1", "__version": 8, "state": "active" }),
153
- ),
154
- set("changes", "c1", json!({ "id": "c1" })),
155
- delete("outbox", "o1"),
156
- ],
157
- )
158
- .expect("the precondition holds, so the transaction commits");
159
-
160
- assert_eq!(commit.rows.len(), 3);
161
- assert_eq!(version_of(&db, "sessions", "s1"), 8);
162
- assert!(present(&db, "changes", "c1"));
163
- assert!(
164
- !present(&db, "outbox", "o1"),
165
- "the staged delete applied too"
166
- );
167
- }
168
-
169
- #[test]
170
- fn a_stale_version_commits_nothing_at_all() {
171
- // The failure the whole primitive exists to prevent: A is checked while
172
- // B and C commit anyway.
173
- let dir = TempDir::new().unwrap();
174
- let db = db(&dir);
175
- seed(&db, "records", "a", 7, json!({}));
176
- seed(&db, "records", "b", 1, json!({ "state": "original" }));
177
-
178
- // A moves to 8 before the transaction is evaluated.
179
- seed(&db, "records", "a", 8, json!({}));
180
- assert_eq!(version_of(&db, "records", "a"), 8);
181
-
182
- let error = guarded(
183
- &db,
184
- "tx-stale",
185
- &[expect_version("records", "a", 7)],
186
- &[
187
- set(
188
- "records",
189
- "a",
190
- json!({ "id": "a", "__version": 8, "written": true }),
191
- ),
192
- set("records", "b", json!({ "id": "b", "state": "overwritten" })),
193
- set("records", "c", json!({ "id": "c" })),
194
- ],
195
- )
196
- .expect_err("a stale version must reject");
197
-
198
- assert_eq!(
199
- failure(error),
200
- PreconditionFailure::Version {
201
- collection: "records".into(),
202
- key: "records:a".into(),
203
- expected: 7,
204
- actual: 8,
205
- },
206
- "and it names the predicate that failed, so a caller can tell a lost race \
207
- from a broken deployment"
208
- );
209
-
210
- assert_eq!(
211
- version_of(&db, "records", "a"),
212
- 8,
213
- "the guarded record is untouched"
214
- );
215
- assert_eq!(
216
- field(&db, "records", "b", "state"),
217
- "original",
218
- "and so is every other staged record"
219
- );
220
- assert!(
221
- !present(&db, "records", "c"),
222
- "a staged write to a new record did not appear"
223
- );
224
- }
225
-
226
- #[test]
227
- fn multi_record_ownership_fencing() {
228
- // The Session Kernel's shape. Owner 1 read generation 12; owner 2 took
229
- // 13 before owner 1 committed. Owner 1 must write nothing -- not the
230
- // session, not the change record, not the outbox row.
231
- let dir = TempDir::new().unwrap();
232
- let db = db(&dir);
233
- seed(&db, "sessions", "session", 12, json!({ "generation": 12 }));
234
-
235
- seed(&db, "sessions", "session", 13, json!({ "generation": 13 }));
236
-
237
- let error = guarded(
238
- &db,
239
- "owner-1-commit",
240
- &[expect_version("sessions", "session", 12)],
241
- &[
242
- set(
243
- "sessions",
244
- "session",
245
- json!({ "id": "session", "__version": 13, "generation": 12 }),
246
- ),
247
- set("changes", "change-1", json!({ "id": "change-1" })),
248
- set("outbox", "outbox-1", json!({ "id": "outbox-1" })),
249
- ],
250
- )
251
- .expect_err("the losing owner must be refused");
252
-
253
- assert!(matches!(
254
- failure(error),
255
- PreconditionFailure::Version {
256
- expected: 12,
257
- actual: 13,
258
- ..
259
- }
260
- ));
261
- assert_eq!(
262
- field(&db, "sessions", "session", "generation"),
263
- 13,
264
- "the winning owner's generation stands"
265
- );
266
- assert!(!present(&db, "changes", "change-1"), "no change record");
267
- assert!(!present(&db, "outbox", "outbox-1"), "no outbox record");
268
- }
269
-
270
- // -- epoch and lease ---------------------------------------------------
271
-
272
- #[test]
273
- fn a_matching_epoch_commits_and_a_stale_one_rejects() {
274
- let dir = TempDir::new().unwrap();
275
- let db = db(&dir);
276
- seed(&db, "cells", "c", 1, json!({ "authority": { "epoch": 4 } }));
277
-
278
- guarded(
279
- &db,
280
- "tx-epoch-ok",
281
- &[RecordPrecondition {
282
- expected_epoch: Some(4),
283
- ..precondition("cells", "c")
284
- }],
285
- &[set("cells", "written", json!({ "id": "written" }))],
286
- )
287
- .expect("a matching epoch commits");
288
- assert!(present(&db, "cells", "written"));
289
-
290
- let error = guarded(
291
- &db,
292
- "tx-epoch-stale",
293
- &[RecordPrecondition {
294
- expected_epoch: Some(3),
295
- ..precondition("cells", "c")
296
- }],
297
- &[set("cells", "never", json!({ "id": "never" }))],
298
- )
299
- .expect_err("a stale epoch rejects");
300
-
301
- assert_eq!(
302
- failure(error),
303
- PreconditionFailure::Epoch {
304
- collection: "cells".into(),
305
- key: "cells:c".into(),
306
- expected: 3,
307
- actual: 4,
308
- }
309
- );
310
- assert!(!present(&db, "cells", "never"), "and writes nothing");
311
- }
312
-
313
- #[test]
314
- fn a_held_lease_commits_and_a_stale_or_expired_one_rejects() {
315
- let dir = TempDir::new().unwrap();
316
- let db = db(&dir);
317
- let future = crate::now_ms() as u64 + 600_000;
318
- seed(
319
- &db,
320
- "cells",
321
- "c",
322
- 1,
323
- json!({ "lease": { "leaseId": "lease-a", "expiresAt": future } }),
324
- );
325
-
326
- let hold = |lease: &str| RecordPrecondition {
327
- expected_lease_id: Some(lease.to_string()),
328
- ..precondition("cells", "c")
329
- };
330
-
331
- guarded(
332
- &db,
333
- "tx-lease-ok",
334
- &[hold("lease-a")],
335
- &[set("cells", "w1", json!({ "id": "w1" }))],
336
- )
337
- .expect("the held lease commits");
338
- assert!(present(&db, "cells", "w1"));
339
-
340
- let error = guarded(
341
- &db,
342
- "tx-lease-stale",
343
- &[hold("lease-b")],
344
- &[set("cells", "w2", json!({ "id": "w2" }))],
345
- )
346
- .expect_err("a different lease rejects");
347
- assert_eq!(
348
- failure(error),
349
- PreconditionFailure::Lease {
350
- collection: "cells".into(),
351
- key: "cells:c".into(),
352
- expected: "lease-b".into(),
353
- actual: Some("lease-a".into()),
354
- }
355
- );
356
- assert!(!present(&db, "cells", "w2"));
357
-
358
- // An expired lease is not held, so naming it is a conflict: the caller
359
- // believes it owns something it no longer does.
360
- seed(
361
- &db,
362
- "cells",
363
- "expired",
364
- 1,
365
- json!({ "lease": { "leaseId": "lease-a", "expiresAt": 1u64 } }),
366
- );
367
- let error = guarded(
368
- &db,
369
- "tx-lease-expired",
370
- &[RecordPrecondition {
371
- expected_lease_id: Some("lease-a".into()),
372
- ..precondition("cells", "expired")
373
- }],
374
- &[set("cells", "w3", json!({ "id": "w3" }))],
375
- )
376
- .expect_err("an expired lease is not held");
377
- assert!(matches!(failure(error), PreconditionFailure::Lease { .. }));
378
- assert!(!present(&db, "cells", "w3"));
379
- }
380
-
381
- #[test]
382
- fn a_transaction_rejects_if_any_predicate_of_several_fails() {
383
- let dir = TempDir::new().unwrap();
384
- let db = db(&dir);
385
- seed(&db, "cells", "c", 5, json!({ "authority": { "epoch": 2 } }));
386
-
387
- // Version right, epoch wrong.
388
- let error = guarded(
389
- &db,
390
- "tx-mixed",
391
- &[RecordPrecondition {
392
- expected_version: Some(5),
393
- expected_epoch: Some(9),
394
- ..precondition("cells", "c")
395
- }],
396
- &[set("cells", "w", json!({ "id": "w" }))],
397
- )
398
- .expect_err("one failing predicate refuses the whole transaction");
399
- assert!(matches!(
400
- failure(error),
401
- PreconditionFailure::Epoch {
402
- expected: 9,
403
- actual: 2,
404
- ..
405
- }
406
- ));
407
- assert!(!present(&db, "cells", "w"));
408
-
409
- // Several records, one of them stale.
410
- seed(&db, "cells", "d", 1, json!({}));
411
- let error = guarded(
412
- &db,
413
- "tx-mixed-2",
414
- &[
415
- expect_version("cells", "c", 5),
416
- expect_version("cells", "d", 99),
417
- ],
418
- &[set("cells", "w", json!({ "id": "w" }))],
419
- )
420
- .expect_err("a failing predicate on any record refuses the transaction");
421
- assert!(matches!(
422
- failure(error),
423
- PreconditionFailure::Version { expected: 99, .. }
424
- ));
425
- assert!(!present(&db, "cells", "w"));
426
- }
427
-
428
- // -- shapes that must not be mistaken for protection -------------------
429
-
430
- #[test]
431
- fn a_precondition_on_a_missing_record_is_a_conflict_not_a_pass() {
432
- let dir = TempDir::new().unwrap();
433
- let db = db(&dir);
434
- let error = guarded(
435
- &db,
436
- "tx-missing",
437
- &[expect_version("cells", "absent", 1)],
438
- &[set("cells", "w", json!({ "id": "w" }))],
439
- )
440
- .expect_err("expecting a version of a record that does not exist is a conflict");
441
- assert_eq!(
442
- failure(error),
443
- PreconditionFailure::Missing {
444
- collection: "cells".into(),
445
- key: "cells:absent".into()
446
- }
447
- );
448
- assert!(!present(&db, "cells", "w"));
449
- }
450
-
451
- #[test]
452
- fn an_empty_precondition_is_refused_rather_than_satisfied() {
453
- // A precondition that constrains nothing would read as protection and
454
- // provide none, so it is a caller error rather than a pass.
455
- let dir = TempDir::new().unwrap();
456
- let db = db(&dir);
457
- let error = guarded(
458
- &db,
459
- "tx-empty",
460
- &[precondition("cells", "c")],
461
- &[set("cells", "w", json!({ "id": "w" }))],
462
- )
463
- .expect_err("an empty precondition is refused");
464
- assert!(format!("{error}").contains("EMPTY_PRECONDITION"));
465
- assert!(!present(&db, "cells", "w"));
466
- }
467
-
468
- #[test]
469
- fn require_absent_still_means_absent() {
470
- let dir = TempDir::new().unwrap();
471
- let db = db(&dir);
472
- let absent = |id: &str| RecordPrecondition {
473
- require_absent: true,
474
- ..precondition("cells", id)
475
- };
476
-
477
- guarded(
478
- &db,
479
- "tx-new",
480
- &[absent("fresh")],
481
- &[set("cells", "fresh", json!({ "id": "fresh" }))],
482
- )
483
- .expect("absent record commits");
484
- assert!(present(&db, "cells", "fresh"));
485
- assert_eq!(version_of(&db, "cells", "fresh"), 1);
486
-
487
- let error = guarded(
488
- &db,
489
- "tx-again",
490
- &[absent("fresh")],
491
- &[set("cells", "other", json!({ "id": "other" }))],
492
- )
493
- .expect_err("an existing record refuses a require-absent transaction");
494
- assert_eq!(
495
- failure(error),
496
- PreconditionFailure::Present {
497
- collection: "cells".into(),
498
- key: "cells:fresh".into()
499
- }
500
- );
501
- assert!(!present(&db, "cells", "other"));
502
- }
503
-
504
- // -- replay ------------------------------------------------------------
505
-
506
- #[test]
507
- fn a_committed_guarded_transaction_replays_as_a_duplicate() {
508
- let dir = TempDir::new().unwrap();
509
- let db = db(&dir);
510
- seed(&db, "cells", "c", 3, json!({}));
511
-
512
- let first = guarded(
513
- &db,
514
- "tx-replay",
515
- &[expect_version("cells", "c", 3)],
516
- &[set("cells", "w", json!({ "id": "w", "n": 1 }))],
517
- )
518
- .expect("commits");
519
- assert!(!first.duplicate);
520
-
521
- // The response was lost; the caller retries the same id. The
522
- // precondition is no longer satisfiable -- but the transaction already
523
- // committed, so replay returns the original outcome rather than
524
- // re-evaluating it.
525
- seed(&db, "cells", "c", 4, json!({}));
526
- let retry = guarded(
527
- &db,
528
- "tx-replay",
529
- &[expect_version("cells", "c", 3)],
530
- &[set("cells", "w", json!({ "id": "w", "n": 2 }))],
531
- )
532
- .expect("a committed id replays rather than failing");
533
- assert!(
534
- retry.duplicate,
535
- "the retry is recognised as the same transaction"
536
- );
537
- assert_eq!(
538
- field(&db, "cells", "w", "n"),
539
- 1,
540
- "and nothing is written twice"
541
- );
542
- }
543
-
544
- #[test]
545
- fn a_failed_precondition_does_not_consume_the_transaction_id() {
546
- // Stated explicitly rather than left ambiguous: a refused transaction
547
- // never committed, so its id is unused. A caller may retry the same id
548
- // after re-reading state, or use a new one for a new decision. Both
549
- // work, and neither is poisoned by the earlier refusal.
550
- let dir = TempDir::new().unwrap();
551
- let db = db(&dir);
552
- seed(&db, "cells", "c", 1, json!({}));
553
-
554
- guarded(
555
- &db,
556
- "tx-decision",
557
- &[expect_version("cells", "c", 99)],
558
- &[set("cells", "w", json!({ "id": "w" }))],
559
- )
560
- .expect_err("refused");
561
- assert!(!present(&db, "cells", "w"));
562
-
563
- // Same id, corrected expectation.
564
- let commit = guarded(
565
- &db,
566
- "tx-decision",
567
- &[expect_version("cells", "c", 1)],
568
- &[set("cells", "w", json!({ "id": "w" }))],
569
- )
570
- .expect("the same id is still usable after a refusal");
571
- assert!(!commit.duplicate, "it is a first commit, not a replay");
572
- assert!(present(&db, "cells", "w"));
573
- }
574
-
575
- #[test]
576
- fn a_guarded_transaction_survives_restart() {
577
- let dir = TempDir::new().unwrap();
578
- let path = dir.path().join("app.log");
579
- {
580
- let db = FeltDb::open(&path).expect("open");
581
- seed(&db, "cells", "c", 2, json!({}));
582
- guarded(
583
- &db,
584
- "tx-durable",
585
- &[expect_version("cells", "c", 2)],
586
- &[set("cells", "w", json!({ "id": "w" }))],
587
- )
588
- .expect("commits");
589
- }
590
- let reopened = FeltDb::open(&path).expect("reopen");
591
- assert!(
592
- present(&reopened, "cells", "w"),
593
- "a guarded commit is as durable as any other"
594
- );
595
- let retry = reopened
596
- .apply_atomic_transaction_guarded(
597
- "tx-durable",
598
- None,
599
- None,
600
- &[],
601
- &[expect_version("cells", "c", 2)],
602
- &[set("cells", "w", json!({ "id": "w", "n": 2 }))],
603
- None,
604
- )
605
- .expect("replay after restart");
606
- assert!(retry.duplicate, "transaction identity survives the restart");
607
- }
608
-
609
- // -- concurrency -------------------------------------------------------
610
-
611
- #[test]
612
- fn exactly_one_of_two_competing_transactions_commits() {
613
- // Sequential tests cannot show that the check and the write share a
614
- // boundary. Two threads race on the same expected version; the
615
- // authority must admit exactly one, and the loser must write neither of
616
- // its records.
617
- use std::sync::Arc;
618
- let dir = TempDir::new().unwrap();
619
- let db = Arc::new(db(&dir));
620
- seed(&db, "cells", "shared", 1, json!({}));
621
-
622
- let barrier = Arc::new(std::sync::Barrier::new(2));
623
- let mut handles = Vec::new();
624
- for (id, second) in [("t1", "b"), ("t2", "c")] {
625
- let db = Arc::clone(&db);
626
- let barrier = Arc::clone(&barrier);
627
- handles.push(std::thread::spawn(move || {
628
- barrier.wait();
629
- db.apply_atomic_transaction_guarded(
630
- id,
631
- None,
632
- None,
633
- &[],
634
- &[RecordPrecondition {
635
- expected_version: Some(1),
636
- ..RecordPrecondition {
637
- capability: "cells".into(),
638
- key: "cells:shared".into(),
639
- ..Default::default()
640
- }
641
- }],
642
- &[
643
- set(
644
- "cells",
645
- "shared",
646
- json!({ "id": "shared", "__version": 2, "by": id }),
647
- ),
648
- set("cells", second, json!({ "id": second })),
649
- ],
650
- None,
651
- )
652
- .map(|_| id)
653
- }));
654
- }
655
-
656
- let outcomes: Vec<_> = handles
657
- .into_iter()
658
- .map(|handle| handle.join().unwrap())
659
- .collect();
660
- let winners: Vec<_> = outcomes
661
- .iter()
662
- .filter_map(|outcome| outcome.as_ref().ok())
663
- .collect();
664
- let losers: Vec<_> = outcomes
665
- .iter()
666
- .filter_map(|outcome| outcome.as_ref().err())
667
- .collect();
668
-
669
- assert_eq!(winners.len(), 1, "exactly one transaction may commit");
670
- assert_eq!(losers.len(), 1);
671
- assert!(
672
- matches!(losers[0], FlowError::PreconditionFailed(_)),
673
- "the loser receives a structured conflict, not an infrastructure error: {:?}",
674
- losers[0]
675
- );
676
-
677
- let winner = *winners[0];
678
- assert_eq!(version_of(&db, "cells", "shared"), 2);
679
- assert_eq!(field(&db, "cells", "shared", "by"), winner);
680
-
681
- let loser_record = if winner == "t1" { "c" } else { "b" };
682
- let winner_record = if winner == "t1" { "b" } else { "c" };
683
- assert!(
684
- present(&db, "cells", winner_record),
685
- "the winner's second write committed"
686
- );
687
- assert!(
688
- !present(&db, "cells", loser_record),
689
- "the loser wrote neither of its records"
690
- );
691
- }
692
-
693
- // -- the boundary that used to exist ----------------------------------
694
-
695
- #[test]
696
- fn a_fenced_write_is_fenced_against_its_own_successor() {
697
- // This test previously asserted the opposite, and recorded it as a
698
- // known limitation: because a transaction write stored the caller's
699
- // value verbatim, two transactions fencing on the same version both
700
- // committed, and `ifVersion` was a check that permitted its own
701
- // successor rather than a fence.
702
- //
703
- // The authority now advances the version of a fenced write, so the
704
- // second is refused. The test is kept and inverted rather than deleted,
705
- // because the gap is the reason the contract exists.
706
- let dir = TempDir::new().unwrap();
707
- let db = db(&dir);
708
- seed(&db, "cells", "c", 1, json!({}));
709
-
710
- guarded(
711
- &db,
712
- "tx-a",
713
- &[expect_version("cells", "c", 1)],
714
- &[set("cells", "c", json!({ "id": "c", "written": "a" }))],
715
- )
716
- .expect("first commits");
717
- assert_eq!(version_of(&db, "cells", "c"), 2, "and advances the version");
718
-
719
- let second = guarded(
720
- &db,
721
- "tx-b",
722
- &[expect_version("cells", "c", 1)],
723
- &[set("cells", "c", json!({ "id": "c", "written": "b" }))],
724
- );
725
- assert!(
726
- second.is_err(),
727
- "a caller no longer has to advance __version by hand to be fenced against itself"
728
- );
729
- assert_eq!(field(&db, "cells", "c", "written"), "a");
730
- }
731
-
732
- // -- version advancement ----------------------------------------------
733
- //
734
- // docs/architecture/transaction-version-contract.md: a write fenced by
735
- // expectedVersion is a conditional replacement, and the authority advances
736
- // the version so `ifVersion` is a fence rather than a check that permits
737
- // its own successor.
738
-
739
- #[test]
740
- fn a_version_fenced_write_advances_the_version() {
741
- let dir = TempDir::new().unwrap();
742
- let db = db(&dir);
743
- seed(&db, "cells", "c", 1, json!({}));
744
-
745
- guarded(
746
- &db,
747
- "tx-advance",
748
- &[expect_version("cells", "c", 1)],
749
- &[set("cells", "c", json!({ "id": "c", "state": "written" }))],
750
- )
751
- .expect("commits");
752
-
753
- assert_eq!(
754
- version_of(&db, "cells", "c"),
755
- 2,
756
- "validated at 1, written at 2"
757
- );
758
- assert_eq!(field(&db, "cells", "c", "state"), "written");
759
- }
760
-
761
- #[test]
762
- fn the_authority_owns_the_transition_and_replaces_a_supplied_version() {
763
- // A caller that computes a different next version must not be able to
764
- // install it, exactly as updateIfVersion strips __version from updates.
765
- let dir = TempDir::new().unwrap();
766
- let db = db(&dir);
767
- seed(&db, "cells", "c", 4, json!({}));
768
-
769
- guarded(
770
- &db,
771
- "tx-owned",
772
- &[expect_version("cells", "c", 4)],
773
- &[set("cells", "c", json!({ "id": "c", "__version": 99 }))],
774
- )
775
- .expect("commits");
776
- assert_eq!(
777
- version_of(&db, "cells", "c"),
778
- 5,
779
- "the authority's transition, not the caller's"
780
- );
781
- }
782
-
783
- #[test]
784
- fn a_second_transaction_on_the_same_version_now_fails() {
785
- // The gap this closes: before advancement, both of these committed.
786
- let dir = TempDir::new().unwrap();
787
- let db = db(&dir);
788
- seed(&db, "cells", "c", 7, json!({}));
789
-
790
- guarded(
791
- &db,
792
- "tx-first",
793
- &[expect_version("cells", "c", 7)],
794
- &[set("cells", "c", json!({ "id": "c", "by": "first" }))],
795
- )
796
- .expect("the first commits");
797
-
798
- let second = guarded(
799
- &db,
800
- "tx-second",
801
- &[expect_version("cells", "c", 7)],
802
- &[set("cells", "c", json!({ "id": "c", "by": "second" }))],
803
- );
804
- assert!(
805
- second.is_err(),
806
- "ifVersion is now a fence: the successor is refused"
807
- );
808
- assert!(matches!(
809
- failure(second.unwrap_err()),
810
- PreconditionFailure::Version {
811
- expected: 7,
812
- actual: 8,
813
- ..
814
- }
815
- ));
816
- assert_eq!(field(&db, "cells", "c", "by"), "first");
817
- }
818
-
819
- #[test]
820
- fn sequential_transactions_each_advancing_both_succeed() {
821
- let dir = TempDir::new().unwrap();
822
- let db = db(&dir);
823
- seed(&db, "cells", "c", 1, json!({}));
824
-
825
- guarded(
826
- &db,
827
- "s1",
828
- &[expect_version("cells", "c", 1)],
829
- &[set("cells", "c", json!({ "id": "c" }))],
830
- )
831
- .expect("first");
832
- guarded(
833
- &db,
834
- "s2",
835
- &[expect_version("cells", "c", 2)],
836
- &[set("cells", "c", json!({ "id": "c" }))],
837
- )
838
- .expect("second, on the advanced version");
839
- assert_eq!(version_of(&db, "cells", "c"), 3);
840
- }
841
-
842
- #[test]
843
- fn each_versioned_record_in_one_transaction_advances_independently() {
844
- let dir = TempDir::new().unwrap();
845
- let db = db(&dir);
846
- seed(&db, "cells", "a", 1, json!({}));
847
- seed(&db, "cells", "b", 5, json!({}));
848
-
849
- guarded(
850
- &db,
851
- "tx-multi",
852
- &[
853
- expect_version("cells", "a", 1),
854
- expect_version("cells", "b", 5),
855
- ],
856
- &[
857
- set("cells", "a", json!({ "id": "a" })),
858
- set("cells", "b", json!({ "id": "b" })),
859
- set("cells", "c", json!({ "id": "c" })),
860
- ],
861
- )
862
- .expect("commits");
863
-
864
- assert_eq!(version_of(&db, "cells", "a"), 2);
865
- assert_eq!(version_of(&db, "cells", "b"), 6);
866
- assert_eq!(
867
- version_of(&db, "cells", "c"),
868
- 0,
869
- "an unfenced write in the same transaction is stored verbatim and gains no version"
870
- );
871
- }
872
-
873
- #[test]
874
- fn a_guard_only_record_is_not_advanced() {
875
- let dir = TempDir::new().unwrap();
876
- let db = db(&dir);
877
- seed(&db, "sessions", "s", 3, json!({}));
878
-
879
- guarded(
880
- &db,
881
- "tx-guard-only",
882
- &[expect_version("sessions", "s", 3)],
883
- &[set("changes", "c1", json!({ "id": "c1" }))],
884
- )
885
- .expect("commits");
886
-
887
- assert_eq!(
888
- version_of(&db, "sessions", "s"),
889
- 3,
890
- "a guard is read, not written: its version is untouched"
891
- );
892
- assert!(present(&db, "changes", "c1"));
893
- }
894
-
895
- #[test]
896
- fn an_unconditional_write_is_stored_verbatim() {
897
- // What preserves every existing transaction caller.
898
- let dir = TempDir::new().unwrap();
899
- let db = db(&dir);
900
- seed(&db, "cells", "c", 4, json!({}));
901
-
902
- db.apply_atomic_transaction(
903
- "tx-plain",
904
- None,
905
- &[],
906
- &[set(
907
- "cells",
908
- "c",
909
- json!({ "id": "c", "__version": 4, "state": "same" }),
910
- )],
911
- None,
912
- )
913
- .expect("commits");
914
- assert_eq!(
915
- version_of(&db, "cells", "c"),
916
- 4,
917
- "unchanged, because nothing fenced it"
918
- );
919
-
920
- db.apply_atomic_transaction(
921
- "tx-plain-2",
922
- None,
923
- &[],
924
- &[set("cells", "unversioned", json!({ "id": "unversioned" }))],
925
- None,
926
- )
927
- .expect("commits");
928
- assert_eq!(
929
- version_of(&db, "cells", "unversioned"),
930
- 0,
931
- "and the authority assigns no version to a record that has none"
932
- );
933
- }
934
-
935
- #[test]
936
- fn a_fenced_delete_validates_and_removes_without_a_version_transition() {
937
- let dir = TempDir::new().unwrap();
938
- let db = db(&dir);
939
- seed(&db, "cells", "c", 2, json!({}));
940
-
941
- let stale = guarded(
942
- &db,
943
- "tx-del-stale",
944
- &[expect_version("cells", "c", 1)],
945
- &[delete("cells", "c")],
946
- );
947
- assert!(stale.is_err(), "a stale fence refuses the delete");
948
- assert!(present(&db, "cells", "c"));
949
-
950
- guarded(
951
- &db,
952
- "tx-del",
953
- &[expect_version("cells", "c", 2)],
954
- &[delete("cells", "c")],
955
- )
956
- .expect("the current fence permits it");
957
- assert!(
958
- !present(&db, "cells", "c"),
959
- "fence, then delete; there is no record left to version"
960
- );
961
- }
962
-
963
- #[test]
964
- fn a_transaction_create_gets_authority_version_one_and_can_be_fenced_immediately() {
965
- let dir = TempDir::new().unwrap();
966
- let db = db(&dir);
967
-
968
- guarded(
969
- &db,
970
- "tx-insert",
971
- &[RecordPrecondition {
972
- require_absent: true,
973
- ..precondition("cells", "new")
974
- }],
975
- &[set(
976
- "cells",
977
- "new",
978
- json!({ "id": "new", "__version": 999, "state": "created" }),
979
- )],
980
- )
981
- .expect("commits");
982
- assert_eq!(
983
- version_of(&db, "cells", "new"),
984
- 1,
985
- "the authority owns the initial version"
986
- );
987
-
988
- guarded(
989
- &db,
990
- "tx-update-new",
991
- &[expect_version("cells", "new", 1)],
992
- &[set(
993
- "cells",
994
- "new",
995
- json!({ "id": "new", "state": "updated" }),
996
- )],
997
- )
998
- .expect("the newly created version can be fenced immediately");
999
- assert_eq!(version_of(&db, "cells", "new"), 2);
1000
-
1001
- let missing = guarded(
1002
- &db,
1003
- "tx-fence-absent",
1004
- &[expect_version("cells", "absent", 1)],
1005
- &[set("cells", "absent", json!({ "id": "absent" }))],
1006
- );
1007
- assert!(
1008
- matches!(
1009
- failure(missing.unwrap_err()),
1010
- PreconditionFailure::Missing { .. }
1011
- ),
1012
- "a version-fenced write against an absent record is a conflict, not an insert"
1013
- );
1014
- }
1015
-
1016
- #[test]
1017
- fn replayed_conditional_create_does_not_create_or_advance_twice() {
1018
- let dir = TempDir::new().unwrap();
1019
- let db = db(&dir);
1020
- let absent = RecordPrecondition {
1021
- require_absent: true,
1022
- ..precondition("cells", "new")
1023
- };
1024
-
1025
- let first = guarded(
1026
- &db,
1027
- "tx-create-once",
1028
- &[absent.clone()],
1029
- &[set("cells", "new", json!({ "id": "new", "delivery": 1 }))],
1030
- )
1031
- .expect("first delivery commits");
1032
- assert!(!first.duplicate);
1033
-
1034
- let replay = guarded(
1035
- &db,
1036
- "tx-create-once",
1037
- &[absent],
1038
- &[set("cells", "new", json!({ "id": "new", "delivery": 2 }))],
1039
- )
1040
- .expect("replay returns the original transaction result");
1041
- assert!(replay.duplicate);
1042
- assert_eq!(version_of(&db, "cells", "new"), 1);
1043
- assert_eq!(field(&db, "cells", "new", "delivery"), 1);
1044
- }
1045
-
1046
- #[test]
1047
- fn replay_of_a_fenced_transaction_does_not_advance_twice() {
1048
- let dir = TempDir::new().unwrap();
1049
- let db = db(&dir);
1050
- seed(&db, "cells", "c", 1, json!({}));
1051
-
1052
- guarded(
1053
- &db,
1054
- "tx-once",
1055
- &[expect_version("cells", "c", 1)],
1056
- &[set("cells", "c", json!({ "id": "c" }))],
1057
- )
1058
- .expect("commits");
1059
- assert_eq!(version_of(&db, "cells", "c"), 2);
1060
-
1061
- let replay = guarded(
1062
- &db,
1063
- "tx-once",
1064
- &[expect_version("cells", "c", 1)],
1065
- &[set("cells", "c", json!({ "id": "c" }))],
1066
- )
1067
- .expect("a committed id replays");
1068
- assert!(replay.duplicate);
1069
- assert_eq!(
1070
- version_of(&db, "cells", "c"),
1071
- 2,
1072
- "a replay cannot advance the version again"
1073
- );
1074
- }
1075
-
1076
- #[test]
1077
- fn competing_fenced_transactions_leave_the_version_at_exactly_one_advance() {
1078
- use std::sync::Arc;
1079
- let dir = TempDir::new().unwrap();
1080
- let db = Arc::new(db(&dir));
1081
- seed(&db, "cells", "race", 1, json!({}));
1082
-
1083
- let barrier = Arc::new(std::sync::Barrier::new(2));
1084
- let mut handles = Vec::new();
1085
- for id in ["r1", "r2"] {
1086
- let db = Arc::clone(&db);
1087
- let barrier = Arc::clone(&barrier);
1088
- handles.push(std::thread::spawn(move || {
1089
- barrier.wait();
1090
- db.apply_atomic_transaction_guarded(
1091
- id,
1092
- None,
1093
- None,
1094
- &[],
1095
- &[RecordPrecondition {
1096
- expected_version: Some(1),
1097
- ..RecordPrecondition {
1098
- capability: "cells".into(),
1099
- key: "cells:race".into(),
1100
- ..Default::default()
1101
- }
1102
- }],
1103
- &[set("cells", "race", json!({ "id": "race", "by": id }))],
1104
- None,
1105
- )
1106
- .map(|_| id)
1107
- }));
1108
- }
1109
- let outcomes: Vec<_> = handles
1110
- .into_iter()
1111
- .map(|handle| handle.join().unwrap())
1112
- .collect();
1113
- assert_eq!(outcomes.iter().filter(|outcome| outcome.is_ok()).count(), 1);
1114
- assert_eq!(
1115
- version_of(&db, "cells", "race"),
1116
- 2,
1117
- "exactly one advance, whichever transaction won"
1118
- );
1119
- }
1120
- }