@lix-js/sdk 0.6.0-preview.1 → 0.6.0-preview.2

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 (191) hide show
  1. package/SKILL.md +305 -320
  2. package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -0
  3. package/dist/engine-wasm/wasm/lix_engine.js +9 -13
  4. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  5. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +1 -0
  6. package/dist/open-lix.d.ts +103 -14
  7. package/dist/open-lix.js +3 -0
  8. package/dist/sqlite/index.js +99 -22
  9. package/dist-engine-src/README.md +18 -0
  10. package/dist-engine-src/src/backend/kv.rs +358 -0
  11. package/dist-engine-src/src/backend/mod.rs +12 -0
  12. package/dist-engine-src/src/backend/testing.rs +658 -0
  13. package/dist-engine-src/src/backend/types.rs +96 -0
  14. package/dist-engine-src/src/binary_cas/chunking.rs +31 -0
  15. package/dist-engine-src/src/binary_cas/codec.rs +346 -0
  16. package/dist-engine-src/src/binary_cas/context.rs +139 -0
  17. package/dist-engine-src/src/binary_cas/kv.rs +1063 -0
  18. package/dist-engine-src/src/binary_cas/mod.rs +11 -0
  19. package/dist-engine-src/src/binary_cas/types.rs +127 -0
  20. package/dist-engine-src/src/cel/context.rs +86 -0
  21. package/dist-engine-src/src/cel/error.rs +19 -0
  22. package/dist-engine-src/src/cel/mod.rs +8 -0
  23. package/dist-engine-src/src/cel/provider.rs +9 -0
  24. package/dist-engine-src/src/cel/runtime.rs +167 -0
  25. package/dist-engine-src/src/cel/value.rs +50 -0
  26. package/dist-engine-src/src/changelog/codec.rs +321 -0
  27. package/dist-engine-src/src/changelog/context.rs +92 -0
  28. package/dist-engine-src/src/changelog/materialization.rs +121 -0
  29. package/dist-engine-src/src/changelog/mod.rs +13 -0
  30. package/dist-engine-src/src/changelog/reader.rs +20 -0
  31. package/dist-engine-src/src/changelog/storage.rs +220 -0
  32. package/dist-engine-src/src/changelog/types.rs +38 -0
  33. package/dist-engine-src/src/commit_graph/context.rs +1588 -0
  34. package/dist-engine-src/src/commit_graph/mod.rs +12 -0
  35. package/dist-engine-src/src/commit_graph/types.rs +145 -0
  36. package/dist-engine-src/src/commit_graph/walker.rs +780 -0
  37. package/dist-engine-src/src/common/error.rs +313 -0
  38. package/dist-engine-src/src/common/fingerprint.rs +3 -0
  39. package/dist-engine-src/src/common/fs_path.rs +1336 -0
  40. package/dist-engine-src/src/common/identity.rs +135 -0
  41. package/dist-engine-src/src/common/metadata.rs +35 -0
  42. package/dist-engine-src/src/common/mod.rs +23 -0
  43. package/dist-engine-src/src/common/types.rs +105 -0
  44. package/dist-engine-src/src/common/wire.rs +222 -0
  45. package/dist-engine-src/src/engine.rs +239 -0
  46. package/dist-engine-src/src/entity_identity.rs +285 -0
  47. package/dist-engine-src/src/functions/context.rs +327 -0
  48. package/dist-engine-src/src/functions/deterministic.rs +113 -0
  49. package/dist-engine-src/src/functions/mod.rs +18 -0
  50. package/dist-engine-src/src/functions/provider.rs +130 -0
  51. package/dist-engine-src/src/functions/state.rs +363 -0
  52. package/dist-engine-src/src/functions/types.rs +37 -0
  53. package/dist-engine-src/src/init.rs +505 -0
  54. package/dist-engine-src/src/json_store/compression.rs +77 -0
  55. package/dist-engine-src/src/json_store/context.rs +129 -0
  56. package/dist-engine-src/src/json_store/encoded.rs +15 -0
  57. package/dist-engine-src/src/json_store/mod.rs +9 -0
  58. package/dist-engine-src/src/json_store/store.rs +236 -0
  59. package/dist-engine-src/src/json_store/types.rs +52 -0
  60. package/dist-engine-src/src/lib.rs +61 -0
  61. package/dist-engine-src/src/live_state/context.rs +2241 -0
  62. package/dist-engine-src/src/live_state/mod.rs +15 -0
  63. package/dist-engine-src/src/live_state/overlay.rs +75 -0
  64. package/dist-engine-src/src/live_state/reader.rs +23 -0
  65. package/dist-engine-src/src/live_state/types.rs +239 -0
  66. package/dist-engine-src/src/live_state/visibility.rs +218 -0
  67. package/dist-engine-src/src/plugin/archive.rs +441 -0
  68. package/dist-engine-src/src/plugin/component.rs +183 -0
  69. package/dist-engine-src/src/plugin/install.rs +637 -0
  70. package/dist-engine-src/src/plugin/manifest.rs +516 -0
  71. package/dist-engine-src/src/plugin/materializer.rs +477 -0
  72. package/dist-engine-src/src/plugin/mod.rs +33 -0
  73. package/dist-engine-src/src/plugin/plugin_manifest.json +119 -0
  74. package/dist-engine-src/src/plugin/storage.rs +74 -0
  75. package/dist-engine-src/src/schema/annotations/defaults.rs +280 -0
  76. package/dist-engine-src/src/schema/annotations/mod.rs +1 -0
  77. package/dist-engine-src/src/schema/builtin/lix_account.json +22 -0
  78. package/dist-engine-src/src/schema/builtin/lix_active_account.json +30 -0
  79. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +30 -0
  80. package/dist-engine-src/src/schema/builtin/lix_change.json +62 -0
  81. package/dist-engine-src/src/schema/builtin/lix_change_author.json +46 -0
  82. package/dist-engine-src/src/schema/builtin/lix_change_set.json +18 -0
  83. package/dist-engine-src/src/schema/builtin/lix_change_set_element.json +75 -0
  84. package/dist-engine-src/src/schema/builtin/lix_commit.json +62 -0
  85. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +46 -0
  86. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +53 -0
  87. package/dist-engine-src/src/schema/builtin/lix_entity_label.json +63 -0
  88. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +53 -0
  89. package/dist-engine-src/src/schema/builtin/lix_key_value.json +41 -0
  90. package/dist-engine-src/src/schema/builtin/lix_label.json +22 -0
  91. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +31 -0
  92. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +35 -0
  93. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +49 -0
  94. package/dist-engine-src/src/schema/builtin/mod.rs +271 -0
  95. package/dist-engine-src/src/schema/definition.json +157 -0
  96. package/dist-engine-src/src/schema/definition.rs +636 -0
  97. package/dist-engine-src/src/schema/key.rs +206 -0
  98. package/dist-engine-src/src/schema/mod.rs +20 -0
  99. package/dist-engine-src/src/schema/seed.rs +14 -0
  100. package/dist-engine-src/src/schema/tests.rs +739 -0
  101. package/dist-engine-src/src/schema_registry.rs +294 -0
  102. package/dist-engine-src/src/session/context.rs +366 -0
  103. package/dist-engine-src/src/session/create_version.rs +80 -0
  104. package/dist-engine-src/src/session/execute.rs +447 -0
  105. package/dist-engine-src/src/session/merge/analysis.rs +102 -0
  106. package/dist-engine-src/src/session/merge/apply.rs +23 -0
  107. package/dist-engine-src/src/session/merge/conflicts.rs +62 -0
  108. package/dist-engine-src/src/session/merge/mod.rs +11 -0
  109. package/dist-engine-src/src/session/merge/stats.rs +65 -0
  110. package/dist-engine-src/src/session/merge/version.rs +437 -0
  111. package/dist-engine-src/src/session/mod.rs +25 -0
  112. package/dist-engine-src/src/session/switch_version.rs +121 -0
  113. package/dist-engine-src/src/sql2/change_provider.rs +337 -0
  114. package/dist-engine-src/src/sql2/classify.rs +147 -0
  115. package/dist-engine-src/src/sql2/commit_derived_provider.rs +591 -0
  116. package/dist-engine-src/src/sql2/context.rs +307 -0
  117. package/dist-engine-src/src/sql2/directory_history_provider.rs +623 -0
  118. package/dist-engine-src/src/sql2/directory_provider.rs +2405 -0
  119. package/dist-engine-src/src/sql2/dml.rs +148 -0
  120. package/dist-engine-src/src/sql2/entity_history_provider.rs +444 -0
  121. package/dist-engine-src/src/sql2/entity_provider.rs +2700 -0
  122. package/dist-engine-src/src/sql2/error.rs +196 -0
  123. package/dist-engine-src/src/sql2/execute.rs +3379 -0
  124. package/dist-engine-src/src/sql2/file_history_provider.rs +902 -0
  125. package/dist-engine-src/src/sql2/file_provider.rs +3254 -0
  126. package/dist-engine-src/src/sql2/filesystem_planner.rs +1526 -0
  127. package/dist-engine-src/src/sql2/filesystem_predicates.rs +159 -0
  128. package/dist-engine-src/src/sql2/filesystem_visibility.rs +369 -0
  129. package/dist-engine-src/src/sql2/history_projection.rs +80 -0
  130. package/dist-engine-src/src/sql2/history_provider.rs +418 -0
  131. package/dist-engine-src/src/sql2/history_route.rs +643 -0
  132. package/dist-engine-src/src/sql2/lix_state_provider.rs +2430 -0
  133. package/dist-engine-src/src/sql2/mod.rs +43 -0
  134. package/dist-engine-src/src/sql2/read_only.rs +65 -0
  135. package/dist-engine-src/src/sql2/record_batch.rs +17 -0
  136. package/dist-engine-src/src/sql2/result_metadata.rs +29 -0
  137. package/dist-engine-src/src/sql2/runtime.rs +60 -0
  138. package/dist-engine-src/src/sql2/session.rs +135 -0
  139. package/dist-engine-src/src/sql2/udfs/common.rs +295 -0
  140. package/dist-engine-src/src/sql2/udfs/lix_active_version_commit_id.rs +53 -0
  141. package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +47 -0
  142. package/dist-engine-src/src/sql2/udfs/lix_json.rs +100 -0
  143. package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +99 -0
  144. package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +99 -0
  145. package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +82 -0
  146. package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +85 -0
  147. package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +76 -0
  148. package/dist-engine-src/src/sql2/udfs/mod.rs +82 -0
  149. package/dist-engine-src/src/sql2/version_provider.rs +1187 -0
  150. package/dist-engine-src/src/sql2/version_scope.rs +394 -0
  151. package/dist-engine-src/src/sql2/write_normalization.rs +345 -0
  152. package/dist-engine-src/src/storage/context.rs +356 -0
  153. package/dist-engine-src/src/storage/mod.rs +14 -0
  154. package/dist-engine-src/src/storage/read_scope.rs +88 -0
  155. package/dist-engine-src/src/storage/types.rs +501 -0
  156. package/dist-engine-src/src/storage_bench.rs +3406 -0
  157. package/dist-engine-src/src/test_support.rs +81 -0
  158. package/dist-engine-src/src/tracked_state/by_file_index.rs +102 -0
  159. package/dist-engine-src/src/tracked_state/codec.rs +747 -0
  160. package/dist-engine-src/src/tracked_state/context.rs +983 -0
  161. package/dist-engine-src/src/tracked_state/diff.rs +494 -0
  162. package/dist-engine-src/src/tracked_state/materialization.rs +141 -0
  163. package/dist-engine-src/src/tracked_state/merge.rs +474 -0
  164. package/dist-engine-src/src/tracked_state/mod.rs +31 -0
  165. package/dist-engine-src/src/tracked_state/rebuild.rs +771 -0
  166. package/dist-engine-src/src/tracked_state/storage.rs +243 -0
  167. package/dist-engine-src/src/tracked_state/tree.rs +2744 -0
  168. package/dist-engine-src/src/tracked_state/tree_types.rs +176 -0
  169. package/dist-engine-src/src/tracked_state/types.rs +61 -0
  170. package/dist-engine-src/src/transaction/commit.rs +1224 -0
  171. package/dist-engine-src/src/transaction/context.rs +1307 -0
  172. package/dist-engine-src/src/transaction/live_state_overlay.rs +34 -0
  173. package/dist-engine-src/src/transaction/mod.rs +11 -0
  174. package/dist-engine-src/src/transaction/normalization.rs +1026 -0
  175. package/dist-engine-src/src/transaction/schema_resolver.rs +127 -0
  176. package/dist-engine-src/src/transaction/staging.rs +1436 -0
  177. package/dist-engine-src/src/transaction/types.rs +351 -0
  178. package/dist-engine-src/src/transaction/validation.rs +4811 -0
  179. package/dist-engine-src/src/untracked_state/codec.rs +363 -0
  180. package/dist-engine-src/src/untracked_state/context.rs +82 -0
  181. package/dist-engine-src/src/untracked_state/materialization.rs +157 -0
  182. package/dist-engine-src/src/untracked_state/mod.rs +17 -0
  183. package/dist-engine-src/src/untracked_state/storage.rs +348 -0
  184. package/dist-engine-src/src/untracked_state/types.rs +96 -0
  185. package/dist-engine-src/src/version/context.rs +52 -0
  186. package/dist-engine-src/src/version/mod.rs +12 -0
  187. package/dist-engine-src/src/version/refs.rs +421 -0
  188. package/dist-engine-src/src/version/stage_rows.rs +71 -0
  189. package/dist-engine-src/src/version/types.rs +21 -0
  190. package/dist-engine-src/src/wasm/mod.rs +60 -0
  191. package/package.json +68 -64
@@ -0,0 +1,421 @@
1
+ use std::sync::Arc;
2
+
3
+ use serde_json::json;
4
+ use tokio::sync::Mutex;
5
+
6
+ use crate::entity_identity::EntityIdentity;
7
+ use crate::json_store::JsonStoreWriter;
8
+ use crate::storage::{StorageReader, StorageWriteSet};
9
+ use crate::untracked_state::{
10
+ canonicalize_materialized_row, MaterializedUntrackedStateRow, UntrackedStateContext,
11
+ UntrackedStateFilter, UntrackedStateRow, UntrackedStateRowRequest, UntrackedStateScanRequest,
12
+ };
13
+ use crate::version::{VersionHead, VersionRefReader};
14
+ use crate::version::{VERSION_REF_SCHEMA_KEY, VERSION_REF_SCHEMA_VERSION};
15
+ use crate::GLOBAL_VERSION_ID;
16
+ use crate::{LixError, NullableKeyFilter};
17
+
18
+ /// Typed access to moving version heads stored in untracked state.
19
+ ///
20
+ /// Version refs are one of the inputs used by live_state visibility, so this
21
+ /// context deliberately bypasses live_state and reads the underlying untracked
22
+ /// rows directly. That keeps the dependency acyclic:
23
+ /// untracked_state -> version_ref -> live_state.
24
+ pub(super) struct VersionRefContext {
25
+ untracked_state: Arc<UntrackedStateContext>,
26
+ }
27
+
28
+ impl VersionRefContext {
29
+ pub(super) fn new(untracked_state: Arc<UntrackedStateContext>) -> Self {
30
+ Self { untracked_state }
31
+ }
32
+
33
+ /// Creates a version-ref reader over a caller-provided KV store.
34
+ pub(super) fn reader<S>(&self, store: S) -> VersionRefStoreReader<S>
35
+ where
36
+ S: StorageReader,
37
+ {
38
+ VersionRefStoreReader {
39
+ untracked_state: Arc::clone(&self.untracked_state),
40
+ store: Mutex::new(store),
41
+ }
42
+ }
43
+
44
+ /// Creates a version-ref writer over a transaction-local storage write set.
45
+ pub(super) fn writer<'a>(&self, writes: &'a mut StorageWriteSet) -> VersionRefWriter<'a> {
46
+ VersionRefWriter {
47
+ untracked_state: Arc::clone(&self.untracked_state),
48
+ writes,
49
+ }
50
+ }
51
+ }
52
+
53
+ /// Read side for version heads.
54
+ pub(super) struct VersionRefStoreReader<S>
55
+ where
56
+ S: StorageReader,
57
+ {
58
+ untracked_state: Arc<UntrackedStateContext>,
59
+ store: Mutex<S>,
60
+ }
61
+
62
+ impl<S> VersionRefStoreReader<S>
63
+ where
64
+ S: StorageReader,
65
+ {
66
+ pub(crate) async fn load_head(
67
+ &self,
68
+ version_id: &str,
69
+ ) -> Result<Option<VersionHead>, LixError> {
70
+ let mut store = self.store.lock().await;
71
+ let Some(row) = self
72
+ .untracked_state
73
+ .reader(&mut *store as &mut dyn StorageReader)
74
+ .load_row(&UntrackedStateRowRequest {
75
+ schema_key: VERSION_REF_SCHEMA_KEY.to_string(),
76
+ version_id: GLOBAL_VERSION_ID.to_string(),
77
+ entity_id: EntityIdentity::single(version_id),
78
+ file_id: NullableKeyFilter::Null,
79
+ })
80
+ .await?
81
+ else {
82
+ return Ok(None);
83
+ };
84
+
85
+ decode_version_head(version_id, &row)
86
+ }
87
+
88
+ pub(crate) async fn load_head_commit_id(
89
+ &self,
90
+ version_id: &str,
91
+ ) -> Result<Option<String>, LixError> {
92
+ Ok(self.load_head(version_id).await?.map(|head| head.commit_id))
93
+ }
94
+
95
+ pub(crate) async fn scan_heads(&self) -> Result<Vec<VersionHead>, LixError> {
96
+ let mut store = self.store.lock().await;
97
+ let rows = self
98
+ .untracked_state
99
+ .reader(&mut *store as &mut dyn StorageReader)
100
+ .scan_rows(&UntrackedStateScanRequest {
101
+ filter: UntrackedStateFilter {
102
+ schema_keys: vec![VERSION_REF_SCHEMA_KEY.to_string()],
103
+ version_ids: vec![GLOBAL_VERSION_ID.to_string()],
104
+ ..UntrackedStateFilter::default()
105
+ },
106
+ ..UntrackedStateScanRequest::default()
107
+ })
108
+ .await?;
109
+ let mut heads = rows
110
+ .iter()
111
+ .map(|row| {
112
+ let version_id = row.entity_id.as_string()?;
113
+ decode_version_head(&version_id, row)
114
+ })
115
+ .collect::<Result<Vec<_>, _>>()?
116
+ .into_iter()
117
+ .flatten()
118
+ .collect::<Vec<_>>();
119
+ heads.sort_by(|left, right| left.version_id.cmp(&right.version_id));
120
+ Ok(heads)
121
+ }
122
+ }
123
+
124
+ #[async_trait::async_trait]
125
+ impl<S> VersionRefReader for VersionRefStoreReader<S>
126
+ where
127
+ S: StorageReader + Send,
128
+ {
129
+ async fn load_head(&self, version_id: &str) -> Result<Option<VersionHead>, LixError> {
130
+ VersionRefStoreReader::load_head(self, version_id).await
131
+ }
132
+
133
+ async fn load_head_commit_id(&self, version_id: &str) -> Result<Option<String>, LixError> {
134
+ VersionRefStoreReader::load_head_commit_id(self, version_id).await
135
+ }
136
+
137
+ async fn scan_heads(&self) -> Result<Vec<VersionHead>, LixError> {
138
+ VersionRefStoreReader::scan_heads(self).await
139
+ }
140
+ }
141
+
142
+ /// Write side for moving version heads.
143
+ pub(super) struct VersionRefWriter<'a> {
144
+ untracked_state: Arc<UntrackedStateContext>,
145
+ writes: &'a mut StorageWriteSet,
146
+ }
147
+
148
+ impl VersionRefWriter<'_> {
149
+ pub(crate) fn stage_rows(&mut self, rows: &[UntrackedStateRow]) -> Result<(), LixError> {
150
+ self.untracked_state.writer(self.writes).stage_rows(rows)
151
+ }
152
+ }
153
+
154
+ pub(super) fn canonical_version_ref_row(
155
+ writes: &mut StorageWriteSet,
156
+ json_writer: &mut JsonStoreWriter,
157
+ version_id: &str,
158
+ commit_id: &str,
159
+ timestamp: &str,
160
+ ) -> Result<UntrackedStateRow, LixError> {
161
+ let row = version_ref_row(version_id, commit_id, timestamp)?;
162
+ canonicalize_materialized_row(writes, json_writer, &row)
163
+ }
164
+
165
+ fn decode_version_head(
166
+ requested_version_id: &str,
167
+ row: &MaterializedUntrackedStateRow,
168
+ ) -> Result<Option<VersionHead>, LixError> {
169
+ let Some(snapshot_content) = row.snapshot_content.as_deref() else {
170
+ return Ok(None);
171
+ };
172
+ let snapshot =
173
+ serde_json::from_str::<serde_json::Value>(snapshot_content).map_err(|error| {
174
+ LixError::new(
175
+ "LIX_ERROR_UNKNOWN",
176
+ format!("engine2 version-ref snapshot parse failed: {error}"),
177
+ )
178
+ })?;
179
+ let commit_id = snapshot
180
+ .get("commit_id")
181
+ .and_then(serde_json::Value::as_str)
182
+ .ok_or_else(|| {
183
+ LixError::new(
184
+ "LIX_ERROR_UNKNOWN",
185
+ format!("version ref for version '{requested_version_id}' is missing commit_id"),
186
+ )
187
+ })?;
188
+ Ok(Some(VersionHead {
189
+ version_id: requested_version_id.to_string(),
190
+ commit_id: commit_id.to_string(),
191
+ }))
192
+ }
193
+
194
+ fn version_ref_row(
195
+ version_id: &str,
196
+ commit_id: &str,
197
+ timestamp: &str,
198
+ ) -> Result<MaterializedUntrackedStateRow, LixError> {
199
+ let snapshot_content = serde_json::to_string(&json!({
200
+ "id": version_id,
201
+ "commit_id": commit_id,
202
+ }))
203
+ .map_err(|error| {
204
+ LixError::new(
205
+ "LIX_ERROR_UNKNOWN",
206
+ format!("engine2 version-ref snapshot serialization failed: {error}"),
207
+ )
208
+ })?;
209
+
210
+ Ok(MaterializedUntrackedStateRow {
211
+ entity_id: crate::entity_identity::EntityIdentity::single(version_id),
212
+ schema_key: VERSION_REF_SCHEMA_KEY.to_string(),
213
+ file_id: None,
214
+ snapshot_content: Some(snapshot_content),
215
+ metadata: None,
216
+ schema_version: VERSION_REF_SCHEMA_VERSION.to_string(),
217
+ created_at: timestamp.to_string(),
218
+ updated_at: timestamp.to_string(),
219
+ global: true,
220
+ version_id: GLOBAL_VERSION_ID.to_string(),
221
+ })
222
+ }
223
+
224
+ #[cfg(test)]
225
+ mod tests {
226
+ use std::sync::Arc;
227
+
228
+ use crate::backend::testing::UnitTestBackend;
229
+ use crate::json_store::JsonStoreContext;
230
+ use crate::storage::{StorageContext, StorageWriteSet};
231
+ use crate::untracked_state::{
232
+ canonicalize_materialized_row, UntrackedStateContext, UntrackedStateRowRequest,
233
+ };
234
+
235
+ use super::*;
236
+
237
+ #[tokio::test]
238
+ async fn load_head_returns_none_when_missing() {
239
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
240
+ let version_ref = test_version_ref();
241
+
242
+ let head = version_ref
243
+ .reader(storage)
244
+ .load_head("missing-version")
245
+ .await
246
+ .expect("missing version ref should load cleanly");
247
+
248
+ assert_eq!(head, None);
249
+ }
250
+
251
+ #[tokio::test]
252
+ async fn advance_head_writes_untracked_global_ref() {
253
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
254
+ let version_ref = VersionRefContext::new(Arc::new(UntrackedStateContext::new()));
255
+ let mut transaction = storage
256
+ .begin_write_transaction()
257
+ .await
258
+ .expect("transaction should open");
259
+
260
+ let mut writes = StorageWriteSet::new();
261
+ stage_version_head(
262
+ &version_ref,
263
+ &mut writes,
264
+ "version-a",
265
+ "commit-a",
266
+ "2026-01-01T00:00:00Z",
267
+ )
268
+ .expect("version head should advance");
269
+ writes
270
+ .apply(&mut transaction.as_mut())
271
+ .await
272
+ .expect("version head should apply");
273
+ transaction
274
+ .commit()
275
+ .await
276
+ .expect("transaction should commit");
277
+
278
+ let head = version_ref
279
+ .reader(storage.clone())
280
+ .load_head("version-a")
281
+ .await
282
+ .expect("version head should load")
283
+ .expect("version head should exist");
284
+ assert_eq!(head.version_id, "version-a");
285
+ assert_eq!(head.commit_id, "commit-a");
286
+
287
+ let mut reader = UntrackedStateContext::new().reader(storage);
288
+ let row = reader
289
+ .load_row(&UntrackedStateRowRequest {
290
+ schema_key: VERSION_REF_SCHEMA_KEY.to_string(),
291
+ version_id: GLOBAL_VERSION_ID.to_string(),
292
+ entity_id: crate::entity_identity::EntityIdentity::single("version-a"),
293
+ file_id: NullableKeyFilter::Null,
294
+ })
295
+ .await
296
+ .expect("version-ref row should load")
297
+ .expect("version-ref row should exist");
298
+ assert!(row.global);
299
+ assert_eq!(row.created_at, "2026-01-01T00:00:00Z");
300
+ assert_eq!(row.updated_at, "2026-01-01T00:00:00Z");
301
+ }
302
+
303
+ #[tokio::test]
304
+ async fn scan_heads_returns_sorted_version_heads() {
305
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
306
+ let version_ref = test_version_ref();
307
+ let mut transaction = storage
308
+ .begin_write_transaction()
309
+ .await
310
+ .expect("transaction should open");
311
+
312
+ let mut writes = StorageWriteSet::new();
313
+ stage_version_head(
314
+ &version_ref,
315
+ &mut writes,
316
+ "version-b",
317
+ "commit-b",
318
+ "2026-01-01T00:00:00Z",
319
+ )
320
+ .expect("version-b should advance");
321
+ stage_version_head(
322
+ &version_ref,
323
+ &mut writes,
324
+ "version-a",
325
+ "commit-a",
326
+ "2026-01-01T00:00:00Z",
327
+ )
328
+ .expect("version-a should advance");
329
+ writes
330
+ .apply(&mut transaction.as_mut())
331
+ .await
332
+ .expect("version heads should apply");
333
+ transaction
334
+ .commit()
335
+ .await
336
+ .expect("transaction should commit");
337
+
338
+ let heads = version_ref
339
+ .reader(storage)
340
+ .scan_heads()
341
+ .await
342
+ .expect("heads should scan");
343
+
344
+ assert_eq!(
345
+ heads,
346
+ vec![
347
+ VersionHead {
348
+ version_id: "version-a".to_string(),
349
+ commit_id: "commit-a".to_string(),
350
+ },
351
+ VersionHead {
352
+ version_id: "version-b".to_string(),
353
+ commit_id: "commit-b".to_string(),
354
+ },
355
+ ]
356
+ );
357
+ }
358
+
359
+ #[tokio::test]
360
+ async fn malformed_snapshot_errors_clearly() {
361
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
362
+ let untracked_state = UntrackedStateContext::new();
363
+ let version_ref = VersionRefContext::new(Arc::new(UntrackedStateContext::new()));
364
+ let mut transaction = storage
365
+ .begin_write_transaction()
366
+ .await
367
+ .expect("transaction should open");
368
+ let mut row = version_ref_row("version-b", "commit-b", "2026-01-01T00:00:00Z")
369
+ .expect("version-ref row should plan");
370
+ row.snapshot_content = Some("{not-json".to_string());
371
+ let mut writes = StorageWriteSet::new();
372
+ let canonical_row = {
373
+ let mut json_writer = JsonStoreContext::new().writer();
374
+ canonicalize_materialized_row(&mut writes, &mut json_writer, &row)
375
+ .expect("malformed row should canonicalize for test setup")
376
+ };
377
+ untracked_state
378
+ .writer(&mut writes)
379
+ .stage_rows(&[canonical_row])
380
+ .expect("malformed row should write for test setup");
381
+ writes
382
+ .apply(&mut transaction.as_mut())
383
+ .await
384
+ .expect("malformed row should apply for test setup");
385
+ transaction
386
+ .commit()
387
+ .await
388
+ .expect("transaction should commit");
389
+
390
+ let error = version_ref
391
+ .reader(storage)
392
+ .load_head("version-b")
393
+ .await
394
+ .expect_err("malformed snapshot should error");
395
+
396
+ assert!(
397
+ error
398
+ .message
399
+ .contains("engine2 version-ref snapshot parse failed"),
400
+ "unexpected error: {error:?}"
401
+ );
402
+ }
403
+
404
+ fn test_version_ref() -> VersionRefContext {
405
+ VersionRefContext::new(Arc::new(UntrackedStateContext::new()))
406
+ }
407
+
408
+ fn stage_version_head(
409
+ version_ref: &VersionRefContext,
410
+ writes: &mut StorageWriteSet,
411
+ version_id: &str,
412
+ commit_id: &str,
413
+ timestamp: &str,
414
+ ) -> Result<(), LixError> {
415
+ let canonical_row = {
416
+ let mut json_writer = JsonStoreContext::new().writer();
417
+ canonical_version_ref_row(writes, &mut json_writer, version_id, commit_id, timestamp)?
418
+ };
419
+ version_ref.writer(writes).stage_rows(&[canonical_row])
420
+ }
421
+ }
@@ -0,0 +1,71 @@
1
+ use serde_json::json;
2
+
3
+ use crate::entity_identity::EntityIdentity;
4
+ use crate::transaction::types::StageRow;
5
+ use crate::GLOBAL_VERSION_ID;
6
+
7
+ pub(crate) const VERSION_DESCRIPTOR_SCHEMA_KEY: &str = "lix_version_descriptor";
8
+ pub(crate) const VERSION_DESCRIPTOR_SCHEMA_VERSION: &str = "1";
9
+ pub(crate) const VERSION_REF_SCHEMA_KEY: &str = "lix_version_ref";
10
+ pub(crate) const VERSION_REF_SCHEMA_VERSION: &str = "1";
11
+
12
+ pub(crate) fn version_descriptor_stage_row(version_id: &str, name: &str, hidden: bool) -> StageRow {
13
+ StageRow {
14
+ entity_id: Some(EntityIdentity::single(version_id)),
15
+ schema_key: VERSION_DESCRIPTOR_SCHEMA_KEY.to_string(),
16
+ file_id: None,
17
+ snapshot_content: Some(encode_snapshot(json!({
18
+ "id": version_id,
19
+ "name": name,
20
+ "hidden": hidden,
21
+ }))),
22
+ metadata: None,
23
+ origin: None,
24
+ schema_version: VERSION_DESCRIPTOR_SCHEMA_VERSION.to_string(),
25
+ created_at: None,
26
+ updated_at: None,
27
+ global: true,
28
+ change_id: None,
29
+ commit_id: None,
30
+ untracked: false,
31
+ version_id: GLOBAL_VERSION_ID.to_string(),
32
+ }
33
+ }
34
+
35
+ pub(crate) fn version_ref_stage_row(version_id: &str, commit_id: &str) -> StageRow {
36
+ StageRow {
37
+ entity_id: Some(EntityIdentity::single(version_id)),
38
+ schema_key: VERSION_REF_SCHEMA_KEY.to_string(),
39
+ file_id: None,
40
+ snapshot_content: Some(encode_snapshot(json!({
41
+ "id": version_id,
42
+ "commit_id": commit_id,
43
+ }))),
44
+ metadata: None,
45
+ origin: None,
46
+ schema_version: VERSION_REF_SCHEMA_VERSION.to_string(),
47
+ created_at: None,
48
+ updated_at: None,
49
+ global: true,
50
+ change_id: None,
51
+ commit_id: None,
52
+ untracked: true,
53
+ version_id: GLOBAL_VERSION_ID.to_string(),
54
+ }
55
+ }
56
+
57
+ pub(crate) fn version_descriptor_tombstone_row(version_id: &str) -> StageRow {
58
+ let mut row = version_descriptor_stage_row(version_id, "", false);
59
+ row.snapshot_content = None;
60
+ row
61
+ }
62
+
63
+ pub(crate) fn version_ref_tombstone_row(version_id: &str) -> StageRow {
64
+ let mut row = version_ref_stage_row(version_id, "");
65
+ row.snapshot_content = None;
66
+ row
67
+ }
68
+
69
+ fn encode_snapshot(value: serde_json::Value) -> String {
70
+ serde_json::to_string(&value).expect("version snapshot should be serializable")
71
+ }
@@ -0,0 +1,21 @@
1
+ /// Current changelog head for a version.
2
+ #[derive(Debug, Clone, PartialEq, Eq)]
3
+ pub(crate) struct VersionHead {
4
+ pub(crate) version_id: String,
5
+ pub(crate) commit_id: String,
6
+ }
7
+
8
+ /// Typed reader for moving version heads.
9
+ #[async_trait::async_trait]
10
+ pub(crate) trait VersionRefReader: Send + Sync {
11
+ async fn load_head(&self, version_id: &str) -> Result<Option<VersionHead>, crate::LixError>;
12
+
13
+ async fn load_head_commit_id(
14
+ &self,
15
+ version_id: &str,
16
+ ) -> Result<Option<String>, crate::LixError> {
17
+ Ok(self.load_head(version_id).await?.map(|head| head.commit_id))
18
+ }
19
+
20
+ async fn scan_heads(&self) -> Result<Vec<VersionHead>, crate::LixError>;
21
+ }
@@ -0,0 +1,60 @@
1
+ use std::sync::Arc;
2
+
3
+ use async_trait::async_trait;
4
+
5
+ use crate::LixError;
6
+
7
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
8
+ pub struct WasmLimits {
9
+ pub max_memory_bytes: u64,
10
+ pub max_fuel: Option<u64>,
11
+ pub timeout_ms: Option<u64>,
12
+ }
13
+
14
+ impl Default for WasmLimits {
15
+ fn default() -> Self {
16
+ Self {
17
+ max_memory_bytes: 64 * 1024 * 1024,
18
+ max_fuel: None,
19
+ timeout_ms: None,
20
+ }
21
+ }
22
+ }
23
+
24
+ #[async_trait(?Send)]
25
+ pub trait WasmRuntime: Send + Sync {
26
+ async fn init_component(
27
+ &self,
28
+ bytes: Vec<u8>,
29
+ limits: WasmLimits,
30
+ ) -> Result<Arc<dyn WasmComponentInstance>, LixError>;
31
+ }
32
+
33
+ #[async_trait(?Send)]
34
+ pub trait WasmComponentInstance: Send + Sync {
35
+ async fn call(&self, export: &str, input: &[u8]) -> Result<Vec<u8>, LixError>;
36
+
37
+ async fn close(&self) -> Result<(), LixError> {
38
+ Ok(())
39
+ }
40
+ }
41
+
42
+ #[derive(Debug, Default, Clone, Copy)]
43
+ pub struct NoopWasmRuntime;
44
+
45
+ #[async_trait(?Send)]
46
+ impl WasmRuntime for NoopWasmRuntime {
47
+ async fn init_component(
48
+ &self,
49
+ _bytes: Vec<u8>,
50
+ _limits: WasmLimits,
51
+ ) -> Result<Arc<dyn WasmComponentInstance>, LixError> {
52
+ Err(LixError {
53
+ code: "LIX_ERROR_UNKNOWN".to_string(),
54
+ message: "wasm runtime is required to execute plugins; provide a non-noop runtime"
55
+ .to_string(),
56
+ hint: None,
57
+ details: None,
58
+ })
59
+ }
60
+ }