@feltdb/core 0.12.0 → 0.12.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 (138) hide show
  1. package/dist/agent-state.d.ts +489 -0
  2. package/dist/agent-state.d.ts.map +1 -0
  3. package/dist/agent-state.js +993 -0
  4. package/dist/build-identity.json +28 -0
  5. package/dist/cli/commands.js +88 -43
  6. package/dist/collection.d.ts +41 -4
  7. package/dist/collection.d.ts.map +1 -1
  8. package/dist/collection.js +116 -47
  9. package/dist/composition.d.ts.map +1 -1
  10. package/dist/composition.js +11 -1
  11. package/dist/create/build-identity.json +28 -0
  12. package/dist/create/cli.js +3 -3
  13. package/dist/create/create.js +17 -19
  14. package/dist/create/package-versions.js +1 -1
  15. package/dist/create/server-source/Cargo.lock +18 -3
  16. package/dist/create/server-source/Cargo.toml +4 -2
  17. package/dist/create/server-source/crates/feltdb/Cargo.toml +20 -0
  18. package/dist/create/server-source/crates/feltdb/LICENSE +21 -0
  19. package/dist/create/server-source/crates/feltdb/README.md +75 -0
  20. package/dist/create/server-source/crates/feltdb/src/agent_state.rs +320 -0
  21. package/dist/create/server-source/crates/feltdb/src/agent_transition_contract.rs +1545 -0
  22. package/dist/create/server-source/crates/feltdb/src/authority.rs +145 -15
  23. package/dist/create/server-source/crates/feltdb/src/lib.rs +238 -23
  24. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +4 -1
  25. package/dist/create/server-source/crates/feltdb/src/state_model.rs +850 -155
  26. package/dist/create/server-source/crates/feltdb/tests/agent_state_native.rs +1188 -0
  27. package/dist/create/server-source/crates/feltdb/tests/agent_transition_contract_conformance.rs +2884 -0
  28. package/dist/create/server-source/crates/feltdb/tests/authority_transaction_identity.rs +534 -0
  29. package/dist/create/server-source/crates/feltdb/tests/fixtures/agent_transition_corpus.json +23340 -0
  30. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_substrate_completion.rs +31 -13
  31. package/dist/create/server-source/crates/feltdb/tests/state_ref_authority.rs +918 -0
  32. package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +15 -2
  33. package/dist/create/server-source/crates/feltdb-client/Cargo.toml +29 -0
  34. package/dist/create/server-source/crates/feltdb-client/LICENSE +21 -0
  35. package/dist/create/server-source/crates/feltdb-client/README.md +70 -0
  36. package/dist/create/server-source/crates/feltdb-client/src/application.rs +527 -0
  37. package/dist/create/server-source/crates/feltdb-client/src/authority.rs +679 -0
  38. package/dist/create/server-source/crates/feltdb-client/src/control.rs +292 -0
  39. package/dist/create/server-source/crates/feltdb-client/src/error.rs +227 -0
  40. package/dist/create/server-source/crates/feltdb-client/src/events.rs +146 -0
  41. package/dist/create/server-source/crates/feltdb-client/src/lib.rs +466 -0
  42. package/dist/create/server-source/crates/feltdb-client/tests/fixtures/README.md +4 -0
  43. package/dist/create/server-source/crates/feltdb-client/tests/fixtures/private-ca.pem +20 -0
  44. package/dist/create/server-source/crates/feltdb-client/tests/fixtures/server-key.pem +28 -0
  45. package/dist/create/server-source/crates/feltdb-client/tests/fixtures/server.pem +20 -0
  46. package/dist/create/server-source/crates/feltdb-client/tests/private_ca.rs +95 -0
  47. package/dist/create/server-source/crates/feltdb-server/Cargo.toml +24 -1
  48. package/dist/create/server-source/crates/feltdb-server/LICENSE +21 -0
  49. package/dist/create/server-source/crates/feltdb-server/README.md +12 -1
  50. package/dist/create/server-source/crates/feltdb-server/benches/query_shape_observer.rs +136 -0
  51. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +3 -0
  52. package/dist/create/server-source/crates/feltdb-server/src/backup.rs +6 -2
  53. package/dist/create/server-source/crates/feltdb-server/src/lib.rs +21 -0
  54. package/dist/create/server-source/crates/feltdb-server/src/main.rs +75 -8
  55. package/dist/create/server-source/crates/feltdb-server/src/query_shape.rs +1137 -0
  56. package/dist/create/server-source/crates/feltdb-server/tests/fixtures/query-shape-trace.expected.json +300 -0
  57. package/dist/create/server-source/crates/feltdb-server/tests/fixtures/query-shape-trace.json +25 -0
  58. package/dist/create/server-source/crates/feltdb-server/tests/query_shape_trace.rs +128 -0
  59. package/dist/create/server-source/crates/feltdb-server/tests/rust_client.rs +683 -0
  60. package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +3 -1
  61. package/dist/create/template/default-project/README.md +12 -7
  62. package/dist/create/template/default-project/src/pages/Dashboard.tsx +1 -1
  63. package/dist/create/template/dot-feltdb-README.md +3 -3
  64. package/dist/db.d.ts.map +1 -1
  65. package/dist/db.js +43 -17
  66. package/dist/development-runtime-bridge.d.ts.map +1 -1
  67. package/dist/development-runtime-bridge.js +22 -1
  68. package/dist/embedded-transaction.d.ts +75 -8
  69. package/dist/embedded-transaction.d.ts.map +1 -1
  70. package/dist/embedded-transaction.js +124 -18
  71. package/dist/error-codes.d.ts +2 -0
  72. package/dist/error-codes.d.ts.map +1 -1
  73. package/dist/error-codes.js +2 -0
  74. package/dist/feltdb.d.ts +1 -11
  75. package/dist/feltdb.d.ts.map +1 -1
  76. package/dist/file-db.d.ts +6 -0
  77. package/dist/file-db.d.ts.map +1 -1
  78. package/dist/file-db.js +43 -11
  79. package/dist/index-core.d.ts +1 -0
  80. package/dist/index-core.d.ts.map +1 -1
  81. package/dist/index-core.js +1 -0
  82. package/dist/indexeddb-db.d.ts.map +1 -1
  83. package/dist/indexeddb-db.js +65 -41
  84. package/dist/memory-db.d.ts.map +1 -1
  85. package/dist/memory-db.js +29 -8
  86. package/dist/studio/{KeyManagementPanel-DZuSeWBK.js → KeyManagementPanel-D5FMvvU9.js} +1 -0
  87. package/dist/studio/app.d.ts.map +1 -1
  88. package/dist/studio/components/GlobalSearch.d.ts.map +1 -1
  89. package/dist/studio/components/KeyManagementPanel.d.ts.map +1 -1
  90. package/dist/studio/components/KeyManagementPanel.js +1 -1
  91. package/dist/studio/components/RouteErrorBoundary.d.ts +5 -0
  92. package/dist/studio/components/RouteErrorBoundary.d.ts.map +1 -0
  93. package/dist/studio/components/index.js +2 -2
  94. package/dist/studio/{components-BHTKlUgT.js → components-B4eKIUN5.js} +3 -2
  95. package/dist/studio/control-plane/Agents.d.ts.map +1 -1
  96. package/dist/studio/control-plane/Data.d.ts.map +1 -1
  97. package/dist/studio/control-plane/Instance.d.ts.map +1 -1
  98. package/dist/studio/control-plane/Operations.d.ts.map +1 -1
  99. package/dist/studio/control-plane/Overview.d.ts.map +1 -1
  100. package/dist/studio/control-plane/Schema.d.ts.map +1 -1
  101. package/dist/studio/control-plane/State.d.ts +11 -0
  102. package/dist/studio/control-plane/State.d.ts.map +1 -1
  103. package/dist/studio/control-plane/context.d.ts +4 -1
  104. package/dist/studio/control-plane/context.d.ts.map +1 -1
  105. package/dist/studio/control-plane/index.d.ts +1 -1
  106. package/dist/studio/control-plane/index.d.ts.map +1 -1
  107. package/dist/studio/control-plane/index.js +2 -2
  108. package/dist/studio/control-plane/primitives.d.ts +22 -2
  109. package/dist/studio/control-plane/primitives.d.ts.map +1 -1
  110. package/dist/studio/control-plane-fxMcJn74.js +4173 -0
  111. package/dist/studio/index.js +307 -235
  112. package/dist/studio/studio.css +1 -1
  113. package/dist/studio/utils/control-plane-api.d.ts.map +1 -1
  114. package/dist/studio/utils/control-plane-api.js +4 -3
  115. package/dist/studio/utils/service-api.d.ts.map +1 -1
  116. package/dist/studio/utils/service-api.js +5 -4
  117. package/dist/studio-app/assets/{feltdb_wasm-DYfWAV0E.js → feltdb_wasm-BatwwogY.js} +1 -1
  118. package/dist/studio-app/assets/feltdb_wasm_bg-BX0newOr.wasm +0 -0
  119. package/dist/studio-app/assets/index-CiC8VY-a.css +1 -0
  120. package/dist/studio-app/assets/index-DoVvbIay.js +30 -0
  121. package/dist/studio-app/index.html +2 -2
  122. package/dist/telemetry.d.ts +1 -0
  123. package/dist/telemetry.d.ts.map +1 -1
  124. package/dist/telemetry.js +4 -3
  125. package/dist/transaction-identity.d.ts +62 -0
  126. package/dist/transaction-identity.d.ts.map +1 -0
  127. package/dist/transaction-identity.js +138 -0
  128. package/dist/transaction.d.ts +50 -5
  129. package/dist/transaction.d.ts.map +1 -1
  130. package/dist/transaction.js +28 -0
  131. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  132. package/dist/wasm/package.json +1 -1
  133. package/package.json +2 -2
  134. package/dist/create/server-source/crates/feltdb-wasm/test.db +0 -0
  135. package/dist/studio/control-plane-DP8ljTDO.js +0 -3207
  136. package/dist/studio-app/assets/feltdb_wasm_bg-DHiOUhoQ.wasm +0 -0
  137. package/dist/studio-app/assets/index-6f6KVx5R.js +0 -30
  138. package/dist/studio-app/assets/index-BrsF-kWL.css +0 -1
@@ -0,0 +1,683 @@
1
+ //! The Rust client (`feltdb-client`) against this crate's real server binary: every request goes
2
+ //! over HTTP to a spawned `feltdb-server`, so these tests hold the client and the server to one
3
+ //! wire contract.
4
+
5
+ use std::{
6
+ io::{BufRead, BufReader},
7
+ path::PathBuf,
8
+ process::{Child, Command, Stdio},
9
+ time::{Duration, SystemTime, UNIX_EPOCH},
10
+ };
11
+
12
+ use feltdb_client::{
13
+ path_segment,
14
+ wire::{CanonicalQuery, QueryFilter},
15
+ AppTransaction, BoundedQuery, CasConflict, CasOutcome, CasRequest, Client, Direction, Error,
16
+ Method, Operation, Precondition, PreconditionFailure, ReadRecord, ReadSet, Transaction,
17
+ };
18
+ use serde_json::{json, Map, Value};
19
+
20
+ const MASTER_KEY: &str = "rust-client-development-key";
21
+
22
+ struct Server {
23
+ child: Child,
24
+ url: String,
25
+ directory: PathBuf,
26
+ }
27
+
28
+ impl Server {
29
+ fn start(name: &str, extra: &[&str]) -> Self {
30
+ Self::start_in(directory(name), extra)
31
+ }
32
+
33
+ /// A server requiring authentication, and a key it accepts. The key file is read at startup,
34
+ /// so the key is issued first.
35
+ fn start_authenticated(name: &str) -> (Self, String) {
36
+ let directory = directory(name);
37
+ let key = issue_key(&directory);
38
+ (Self::start_in(directory, &["--auth"]), key)
39
+ }
40
+
41
+ fn start_in(directory: PathBuf, extra: &[&str]) -> Self {
42
+ let data = directory.join("feltdb.log");
43
+ let mut child = Command::new(env!("CARGO_BIN_EXE_feltdb-server"))
44
+ .args([
45
+ "--host",
46
+ "127.0.0.1",
47
+ "--port",
48
+ "0",
49
+ "--namespace",
50
+ "rust-client",
51
+ "--data",
52
+ data.to_str().unwrap(),
53
+ "--keys",
54
+ directory.join("keys.json").to_str().unwrap(),
55
+ "--audit",
56
+ directory.join("audit.log").to_str().unwrap(),
57
+ ])
58
+ .args(extra)
59
+ .env("FELTDB_MASTER_KEY", MASTER_KEY)
60
+ .env(
61
+ "FELTDB_SESSION_SECRET",
62
+ "rust-client-session-secret-of-at-least-32-bytes",
63
+ )
64
+ .stdout(Stdio::piped())
65
+ .stderr(Stdio::null())
66
+ .spawn()
67
+ .unwrap();
68
+ let stdout = child.stdout.take().unwrap();
69
+ let mut lines = BufReader::new(stdout).lines();
70
+ let url = loop {
71
+ let line = lines
72
+ .next()
73
+ .expect("server exited before readiness")
74
+ .unwrap();
75
+ if let Some(rest) = line.split("ready at ").nth(1) {
76
+ break rest.split_whitespace().next().unwrap().to_owned();
77
+ }
78
+ };
79
+ // Keep draining stdout so the server never blocks on a full pipe.
80
+ std::thread::spawn(move || lines.for_each(drop));
81
+ Self {
82
+ child,
83
+ url,
84
+ directory,
85
+ }
86
+ }
87
+
88
+ fn client(&self) -> Client {
89
+ Client::builder(&self.url).build().unwrap()
90
+ }
91
+ }
92
+
93
+ fn directory(name: &str) -> PathBuf {
94
+ let directory = std::env::temp_dir().join(format!(
95
+ "feltdb-rust-client-{name}-{}-{}",
96
+ std::process::id(),
97
+ SystemTime::now()
98
+ .duration_since(UNIX_EPOCH)
99
+ .unwrap()
100
+ .as_nanos()
101
+ ));
102
+ std::fs::create_dir_all(&directory).unwrap();
103
+ directory
104
+ }
105
+
106
+ fn issue_key(directory: &std::path::Path) -> String {
107
+ let output = Command::new(env!("CARGO_BIN_EXE_feltdb-server"))
108
+ .args([
109
+ "keys",
110
+ "create",
111
+ "--keys",
112
+ directory.join("keys.json").to_str().unwrap(),
113
+ "--name",
114
+ "rust-client",
115
+ "--namespace",
116
+ "rust-client",
117
+ "--scope",
118
+ "*",
119
+ ])
120
+ .env("FELTDB_MASTER_KEY", MASTER_KEY)
121
+ .output()
122
+ .unwrap();
123
+ assert!(output.status.success());
124
+ String::from_utf8(output.stdout)
125
+ .unwrap()
126
+ .trim()
127
+ .lines()
128
+ .last()
129
+ .unwrap()
130
+ .to_owned()
131
+ }
132
+
133
+ impl Drop for Server {
134
+ fn drop(&mut self) {
135
+ let _ = self.child.kill();
136
+ let _ = self.child.wait();
137
+ let _ = std::fs::remove_dir_all(&self.directory);
138
+ }
139
+ }
140
+
141
+ fn object(value: Value) -> Map<String, Value> {
142
+ value.as_object().unwrap().clone()
143
+ }
144
+
145
+ #[tokio::test]
146
+ async fn records_transactions_and_conflicts_over_the_direct_authority() {
147
+ let server = Server::start("direct", &[]);
148
+ let client = server.client();
149
+
150
+ // Records.
151
+ let created = client
152
+ .create("orders", Some("o-1"), json!({ "status": "pending" }))
153
+ .await
154
+ .unwrap();
155
+ assert_eq!(created.key, "orders:o-1");
156
+ let again = client
157
+ .create("orders", Some("o-1"), json!({}))
158
+ .await
159
+ .unwrap_err();
160
+ assert_eq!(again.status(), Some(409));
161
+ let updated = client
162
+ .update("orders", "o-1", object(json!({ "total": 5 })))
163
+ .await
164
+ .unwrap();
165
+ assert_eq!(updated.value["status"], "pending");
166
+ assert_eq!(updated.value["total"], 5);
167
+ assert_eq!(
168
+ client.get("orders", "o-1").await.unwrap().unwrap().value["total"],
169
+ 5
170
+ );
171
+ assert_eq!(client.get("orders", "missing").await.unwrap(), None);
172
+ assert_eq!(client.list("orders").await.unwrap().len(), 1);
173
+
174
+ // An atomic, idempotent transaction.
175
+ let ship = Transaction::new("ship-o-2")
176
+ .operation(Operation::create(
177
+ "orders",
178
+ "o-2",
179
+ json!({ "status": "shipped" }),
180
+ ))
181
+ .operation(Operation::create("audit", "a-1", json!({ "order": "o-2" })));
182
+ let first = client.commit(&ship).await.unwrap();
183
+ assert!(!first.duplicate);
184
+ assert_eq!(first.operations, 2);
185
+ let retry = client.commit(&ship).await.unwrap();
186
+ assert!(retry.duplicate, "a resubmitted transaction is recognized");
187
+ assert_eq!(retry.commit_revision, first.commit_revision);
188
+
189
+ // The same id with different content is refused, typed.
190
+ let reused = Transaction::new("ship-o-2").operation(Operation::put(
191
+ "orders",
192
+ "o-2",
193
+ json!({ "status": "lost" }),
194
+ ));
195
+ let refused = client.commit(&reused).await.unwrap_err();
196
+ assert_eq!(refused.code(), Some("IDEMPOTENCY_CONFLICT"));
197
+
198
+ // A lost creation race is a structured precondition failure; nothing is written.
199
+ let race = Transaction::generated()
200
+ .operation(Operation::create("orders", "o-2", json!({})))
201
+ .operation(Operation::put("orders", "o-3", json!({})));
202
+ let lost = client.commit(&race).await.unwrap_err();
203
+ assert!(lost.is_conflict());
204
+ assert!(matches!(
205
+ lost.precondition_failure(),
206
+ Some(PreconditionFailure::Present { key, .. }) if key == "orders:o-2"
207
+ ));
208
+ assert_eq!(client.get("orders", "o-3").await.unwrap(), None);
209
+
210
+ // A version fence on a staged write; o-2 was created at version 1.
211
+ let stale = Transaction::generated()
212
+ .operation(Operation::put("orders", "o-2", json!({ "status": "x" })).expect_version(7));
213
+ assert!(matches!(
214
+ client
215
+ .commit(&stale)
216
+ .await
217
+ .unwrap_err()
218
+ .precondition_failure(),
219
+ Some(PreconditionFailure::Version {
220
+ expected: 7,
221
+ actual: 1,
222
+ ..
223
+ })
224
+ ));
225
+ let fresh = Transaction::generated()
226
+ .operation(
227
+ Operation::put("orders", "o-2", json!({ "status": "delivered" })).expect_version(1),
228
+ )
229
+ .precondition(Precondition::absent("locks", "o-2"));
230
+ client.commit(&fresh).await.unwrap();
231
+
232
+ // Compare-and-set: a lost race is a value.
233
+ match client
234
+ .compare_and_set(
235
+ "orders",
236
+ "o-2",
237
+ &CasRequest::new(1, json!({ "status": "late" })),
238
+ )
239
+ .await
240
+ .unwrap()
241
+ {
242
+ CasOutcome::Conflict {
243
+ conflict: CasConflict::Version,
244
+ current_version,
245
+ ..
246
+ } => assert_eq!(current_version, 2),
247
+ other => panic!("expected a version conflict, got {other:?}"),
248
+ }
249
+ assert!(matches!(
250
+ client
251
+ .compare_and_set(
252
+ "orders",
253
+ "o-2",
254
+ &CasRequest::new(2, json!({ "status": "returned" }))
255
+ )
256
+ .await
257
+ .unwrap(),
258
+ CasOutcome::Updated {
259
+ current_version: 3,
260
+ ..
261
+ }
262
+ ));
263
+ assert_eq!(
264
+ client
265
+ .compare_and_set("orders", "nope", &CasRequest::new(1, json!({})))
266
+ .await
267
+ .unwrap(),
268
+ CasOutcome::NotFound
269
+ );
270
+
271
+ // A read basis fences a transaction on state it did not write.
272
+ let reads = ReadSet {
273
+ records: vec![ReadRecord {
274
+ collection: "orders".into(),
275
+ id: "o-1".into(),
276
+ }],
277
+ predicates: vec![],
278
+ };
279
+ let basis = client.read_basis(&reads).await.unwrap();
280
+ client
281
+ .update("orders", "o-1", object(json!({ "total": 6 })))
282
+ .await
283
+ .unwrap();
284
+ let fenced = Transaction::generated()
285
+ .operation(Operation::put("audit", "a-2", json!({})))
286
+ .fence(basis);
287
+ assert!(matches!(
288
+ client
289
+ .commit(&fenced)
290
+ .await
291
+ .unwrap_err()
292
+ .precondition_failure(),
293
+ Some(PreconditionFailure::Fence { .. })
294
+ ));
295
+
296
+ client.delete("audit", "a-1").await.unwrap();
297
+ assert_eq!(
298
+ client.delete("audit", "a-1").await.unwrap_err().status(),
299
+ Some(404)
300
+ );
301
+ assert!(client.revision().await.unwrap().revision > 0);
302
+ }
303
+
304
+ #[tokio::test]
305
+ async fn the_bounded_query_pages_in_a_total_order() {
306
+ let server = Server::start("query", &[]);
307
+ let client = server.client();
308
+ let mut transaction = Transaction::new("seed");
309
+ for index in 0..5 {
310
+ transaction = transaction.operation(Operation::put(
311
+ "items",
312
+ format!("i-{index}"),
313
+ json!({ "n": index, "kind": if index % 2 == 0 { "even" } else { "odd" } }),
314
+ ));
315
+ }
316
+ client.commit(&transaction).await.unwrap();
317
+
318
+ let mut seen = Vec::new();
319
+ let mut query = BoundedQuery::new("items", 2).order_by("n", Direction::Asc);
320
+ loop {
321
+ let page = client.query(&query).await.unwrap();
322
+ assert!(page.records.len() <= 2);
323
+ seen.extend(
324
+ page.records
325
+ .iter()
326
+ .map(|record| record["n"].as_i64().unwrap()),
327
+ );
328
+ match page.next_cursor {
329
+ Some(cursor) if !page.exhausted => query = query.after(cursor),
330
+ _ => break,
331
+ }
332
+ }
333
+ assert_eq!(seen, vec![0, 1, 2, 3, 4]);
334
+
335
+ let even = client
336
+ .query(
337
+ &BoundedQuery::new("items", 10)
338
+ .eq("kind", json!("even"))
339
+ .order_by("n", Direction::Desc),
340
+ )
341
+ .await
342
+ .unwrap();
343
+ let values: Vec<_> = even
344
+ .records
345
+ .iter()
346
+ .map(|record| record["n"].clone())
347
+ .collect();
348
+ assert_eq!(values, vec![json!(4), json!(2), json!(0)]);
349
+
350
+ let invalid = client
351
+ .query(&BoundedQuery::new("items", 10))
352
+ .await
353
+ .unwrap_err();
354
+ assert_eq!(invalid.code(), Some("INVALID_QUERY"));
355
+ }
356
+
357
+ #[tokio::test]
358
+ async fn the_event_stream_delivers_committed_changes() {
359
+ let server = Server::start("events", &[]);
360
+ let client = server.client();
361
+ let mut events = client.events(None).await.unwrap();
362
+ client
363
+ .commit(&Transaction::new("evt").operation(Operation::put("things", "t-1", json!({}))))
364
+ .await
365
+ .unwrap();
366
+ let event = tokio::time::timeout(Duration::from_secs(10), events.next())
367
+ .await
368
+ .expect("an event within ten seconds")
369
+ .expect("the stream is open")
370
+ .unwrap();
371
+ assert_eq!(event.event, "state.committed");
372
+ assert!(event.json::<Value>().unwrap().is_object());
373
+ assert!(events.last_event_id().is_some());
374
+ }
375
+
376
+ #[tokio::test]
377
+ async fn an_authenticated_server_refuses_without_a_key_and_serves_with_one() {
378
+ let (server, key) = Server::start_authenticated("auth");
379
+ let anonymous = server.client().revision().await.unwrap_err();
380
+ assert_eq!(anonymous.status(), Some(401));
381
+ assert!(matches!(anonymous, Error::Api(_)));
382
+
383
+ let client = Client::builder(&server.url).token(key).build().unwrap();
384
+ client
385
+ .commit(&Transaction::new("authed").operation(Operation::put(
386
+ "notes",
387
+ "n-1",
388
+ json!({ "a": 1 }),
389
+ )))
390
+ .await
391
+ .unwrap();
392
+ assert!(client.get("notes", "n-1").await.unwrap().is_some());
393
+ }
394
+
395
+ #[tokio::test]
396
+ async fn the_canonical_application_api() {
397
+ let server = Server::start("application", &[]);
398
+ let client = server.client();
399
+
400
+ // Provision an application with one collection, through routes this client does not name.
401
+ let tenant = client
402
+ .request(
403
+ Method::POST,
404
+ "/api/tenants",
405
+ Some(&json!({ "name": "Rust client" })),
406
+ )
407
+ .await
408
+ .unwrap();
409
+ let tenant_id = tenant["tenant_id"]
410
+ .as_str()
411
+ .or(tenant["id"].as_str())
412
+ .unwrap();
413
+ let application = client
414
+ .request(
415
+ Method::POST,
416
+ &format!("/api/tenants/{tenant_id}/applications"),
417
+ Some(&json!({ "name": "Orders" })),
418
+ )
419
+ .await
420
+ .unwrap();
421
+ let application_id = application["application_id"]
422
+ .as_str()
423
+ .or(application["id"].as_str())
424
+ .unwrap()
425
+ .to_owned();
426
+ let drafts = format!("/api/applications/{application_id}/drafts");
427
+ let draft = client
428
+ .request(Method::POST, &drafts, Some(&json!({})))
429
+ .await
430
+ .unwrap();
431
+ let draft_id = draft["draft_id"].as_str().unwrap();
432
+ let mut manifest = draft["manifest"].clone();
433
+ manifest["collections"] = json!([{ "name": "Order", "fields": [
434
+ { "name": "status", "type": "text", "required": true },
435
+ { "name": "total", "type": "integer", "required": false }
436
+ ] }]);
437
+ manifest["policies"] = json!([{ "name": "Order", "resource": "Order",
438
+ "read": "authenticated", "write": "authenticated", "capabilities": [] }]);
439
+ let version = draft["version"].as_u64().unwrap().to_string();
440
+ client
441
+ .request_with_headers(
442
+ Method::PUT,
443
+ &format!("{drafts}/{draft_id}"),
444
+ &[("if-version", &version)],
445
+ Some(&json!({ "manifest": manifest })),
446
+ )
447
+ .await
448
+ .unwrap();
449
+ let revision = client
450
+ .request(
451
+ Method::POST,
452
+ &format!("{drafts}/{draft_id}/commit"),
453
+ Some(&json!({})),
454
+ )
455
+ .await
456
+ .unwrap();
457
+ let revision_id = revision["revision_id"].as_str().unwrap();
458
+ let encoded = path_segment(revision_id);
459
+ client
460
+ .request(
461
+ Method::POST,
462
+ &format!("/api/applications/{application_id}/revisions/{encoded}/promote"),
463
+ Some(&json!({ "environment": "production", "reason": "rust client test" })),
464
+ )
465
+ .await
466
+ .unwrap();
467
+
468
+ let app = client.application(&application_id);
469
+ let info = app.discover().await.unwrap();
470
+ assert_eq!(info.revision_id, revision_id);
471
+ assert_eq!(info.application_name.as_deref(), Some("Orders"));
472
+
473
+ let commit = app
474
+ .commit(
475
+ &AppTransaction::new("order-1")
476
+ .insert("Order", "o-1", json!({ "status": "open", "total": 3 }))
477
+ .insert("Order", "o-2", json!({ "status": "closed", "total": 9 })),
478
+ )
479
+ .await
480
+ .unwrap();
481
+ assert!(!commit.result.duplicate);
482
+ assert!(commit.revision_id.starts_with("revision-"));
483
+
484
+ let query: CanonicalQuery =
485
+ serde_json::from_value(json!({ "collection": "Order", "limit": 10 })).unwrap();
486
+ let open = CanonicalQuery {
487
+ filter: Some(QueryFilter::Eq {
488
+ field: "status".into(),
489
+ value: json!("open"),
490
+ }),
491
+ ..query
492
+ };
493
+ let result = app.query(&open).await.unwrap();
494
+ assert_eq!(result.records.len(), 1);
495
+ assert_eq!(result.records[0]["total"], 3);
496
+
497
+ // An invalid document is refused by the contract, with a code.
498
+ let invalid = app
499
+ .commit(&AppTransaction::generated().insert("Order", "o-3", json!({ "total": 1 })))
500
+ .await
501
+ .unwrap_err();
502
+ assert!(invalid.code().is_some(), "{invalid}");
503
+
504
+ // A read basis over application state fences a canonical transaction.
505
+ let basis = app
506
+ .read_basis(&ReadSet {
507
+ records: vec![ReadRecord {
508
+ collection: "Order".into(),
509
+ id: "o-1".into(),
510
+ }],
511
+ predicates: vec![],
512
+ })
513
+ .await
514
+ .unwrap();
515
+ app.commit(
516
+ &AppTransaction::generated()
517
+ .update("Order", "o-2", json!({ "status": "closed", "total": 10 }))
518
+ .fence(basis.clone()),
519
+ )
520
+ .await
521
+ .unwrap();
522
+ app.commit(&AppTransaction::generated().update(
523
+ "Order",
524
+ "o-1",
525
+ json!({ "status": "open", "total": 4 }),
526
+ ))
527
+ .await
528
+ .unwrap();
529
+ let stale = app
530
+ .commit(
531
+ &AppTransaction::generated()
532
+ .update("Order", "o-2", json!({ "status": "closed", "total": 11 }))
533
+ .fence(basis),
534
+ )
535
+ .await
536
+ .unwrap_err();
537
+ assert!(stale.is_conflict(), "{stale}");
538
+
539
+ let replay = app.replay(None, Some(10)).await.unwrap();
540
+ assert_eq!(replay.application_id, application_id);
541
+ assert!(!replay.next_cursor.is_empty());
542
+ }
543
+
544
+ #[tokio::test]
545
+ async fn the_control_plane_and_versioned_canonical_writes() {
546
+ let server = Server::start("control", &[]);
547
+ let client = server.client();
548
+
549
+ let health = client.health().await.unwrap();
550
+ assert_eq!(health.version, env!("CARGO_PKG_VERSION"));
551
+
552
+ // Provision through the typed control plane: tenant, application, draft, commit, promote.
553
+ let control = client.control();
554
+ let tenant = control.create_tenant("Control").await.unwrap();
555
+ assert!(control
556
+ .tenants()
557
+ .await
558
+ .unwrap()
559
+ .iter()
560
+ .any(|listed| listed.id == tenant.id));
561
+ let created = control
562
+ .create_application(&tenant.id, "Inventory")
563
+ .await
564
+ .unwrap();
565
+ let listed = control.applications(&tenant.id).await.unwrap();
566
+ assert!(listed
567
+ .iter()
568
+ .any(|application| application.id == created.id));
569
+ assert_eq!(
570
+ control.application(&created.id).await.unwrap().name,
571
+ "Inventory"
572
+ );
573
+
574
+ let draft = control.create_draft(&created.id, None).await.unwrap();
575
+ let mut manifest = serde_json::to_value(&draft.manifest).unwrap();
576
+ manifest["collections"] = json!([{ "name": "Item", "fields": [
577
+ { "name": "count", "type": "integer", "required": true }
578
+ ] }]);
579
+ manifest["policies"] = json!([{ "name": "Item", "resource": "Item",
580
+ "read": "authenticated", "write": "authenticated", "capabilities": [] }]);
581
+ let manifest = serde_json::from_value(manifest).unwrap();
582
+ let updated = control
583
+ .update_draft(&created.id, &draft.draft_id, draft.version, &manifest)
584
+ .await
585
+ .unwrap();
586
+ // A write at a version already superseded is refused, not applied.
587
+ let stale = control
588
+ .update_draft(&created.id, &draft.draft_id, draft.version, &manifest)
589
+ .await
590
+ .unwrap_err();
591
+ assert!(stale.status().is_some(), "{stale}");
592
+ assert_eq!(
593
+ control
594
+ .draft(&created.id, &draft.draft_id)
595
+ .await
596
+ .unwrap()
597
+ .version,
598
+ updated.version
599
+ );
600
+ assert!(
601
+ control
602
+ .validate_draft(&created.id, &draft.draft_id)
603
+ .await
604
+ .unwrap()
605
+ .valid
606
+ );
607
+ let revision = control
608
+ .commit_draft(&created.id, &draft.draft_id)
609
+ .await
610
+ .unwrap();
611
+ control
612
+ .promote(
613
+ &created.id,
614
+ &revision.revision_id,
615
+ &feltdb_client::Promotion::new("production")
616
+ .reason("rust client test")
617
+ .expect_current(None),
618
+ )
619
+ .await
620
+ .unwrap();
621
+ assert_eq!(
622
+ control
623
+ .revision(&created.id, &revision.revision_id)
624
+ .await
625
+ .unwrap()
626
+ .manifest
627
+ .collections
628
+ .len(),
629
+ 1
630
+ );
631
+ // An online backup is written on the server's host and described by its manifest.
632
+ let bundle = server.directory.join("backup");
633
+ let manifest = control.backup(bundle.to_str().unwrap()).await.unwrap();
634
+ assert!(manifest["entries"].as_array().is_some(), "{manifest}");
635
+ assert!(bundle.exists());
636
+
637
+ // Per-operation versions: an update or delete at a stale version refuses the transaction.
638
+ let app = client.application(&created.id);
639
+ assert_eq!(
640
+ app.discover().await.unwrap().revision_id,
641
+ revision.revision_id
642
+ );
643
+ let before = app.state_version().await.unwrap();
644
+ assert_eq!(before.revision_id, revision.revision_id);
645
+ app.commit(&AppTransaction::generated().insert("Item", "i-1", json!({ "count": 1 })))
646
+ .await
647
+ .unwrap();
648
+ let after = app.state_version().await.unwrap();
649
+ assert!(after.state_version > before.state_version);
650
+
651
+ app.commit(&AppTransaction::generated().update_if_version(
652
+ "Item",
653
+ "i-1",
654
+ json!({ "count": 2 }),
655
+ 1,
656
+ ))
657
+ .await
658
+ .unwrap();
659
+ let stale = app
660
+ .commit(&AppTransaction::generated().update_if_version(
661
+ "Item",
662
+ "i-1",
663
+ json!({ "count": 3 }),
664
+ 1,
665
+ ))
666
+ .await
667
+ .unwrap_err();
668
+ assert!(stale.is_conflict(), "{stale}");
669
+ assert_eq!(stale.code(), Some("PRECONDITION_FAILED"), "{stale}");
670
+ // A fenced delete and an insert of the same id: an atomic replace.
671
+ app.commit(
672
+ &AppTransaction::generated()
673
+ .delete_if_version("Item", "i-1", 2)
674
+ .insert("Item", "i-1", json!({ "count": 9 })),
675
+ )
676
+ .await
677
+ .unwrap();
678
+ let stale = app
679
+ .commit(&AppTransaction::generated().delete_if_version("Item", "i-1", 2))
680
+ .await
681
+ .unwrap_err();
682
+ assert!(stale.is_conflict(), "{stale}");
683
+ }
@@ -2,12 +2,14 @@
2
2
  name = "feltdb-wasm"
3
3
  version.workspace = true
4
4
  edition.workspace = true
5
+ # An application (server) / shipped to npm as @feltdb/wasm, not a library for crates.io.
6
+ publish = false
5
7
 
6
8
  [lib]
7
9
  crate-type = ["cdylib"]
8
10
 
9
11
  [dependencies]
10
- feltdb = { path = "../feltdb" }
12
+ feltdb = { path = "../feltdb", features = ["unstable-internals"] }
11
13
  wasm-bindgen = "0.2"
12
14
  wasm-bindgen-futures = "0.4"
13
15
  serde = { version = "1", features = ["derive"] }