@feltdb/core 0.5.7 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands.js +110 -5
- package/dist/cli/index.js +1 -1
- package/dist/cli/workspace-integration.js +127 -0
- package/dist/create/cli.js +1 -1
- package/dist/create/create.js +21 -0
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +10 -0
- package/dist/create/server-source/crates/feltdb-server/Cargo.toml +1 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +5 -1
- package/dist/create/server-source/crates/feltdb-server/src/authenticated_principal.rs +273 -0
- package/dist/create/server-source/crates/feltdb-server/src/certification_harness.rs +528 -0
- package/dist/create/server-source/crates/feltdb-server/src/delegation_token.rs +472 -0
- package/dist/create/server-source/crates/feltdb-server/src/durable_operations.rs +992 -0
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +9 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +157 -9
- package/dist/create/server-source/crates/feltdb-server/src/managed_diagnostics.rs +413 -0
- package/dist/create/server-source/crates/feltdb-server/src/membership_policy.rs +488 -0
- package/dist/create/server-source/crates/feltdb-server/src/snapshot_cursor.rs +378 -0
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +49 -0
- package/dist/create/server-source/crates/feltdb-server/src/tenant_policies.rs +525 -0
- package/dist/create/server-source/crates/feltdb-server/src/transaction_recovery.rs +461 -0
- package/dist/create/template/dot-feltdb-README.md +58 -0
- package/dist/create/workspace-initialization.js +77 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/studio-app/assets/{feltdb_wasm-h9mxesnH.js → feltdb_wasm-B4wq4mqp.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-Ceyi7l21.wasm +0 -0
- package/dist/studio-app/assets/{index-B_EMnTaE.js → index-LQmvJSq6.js} +2 -2
- package/dist/studio-app/index.html +1 -1
- package/dist/telemetry.js +1 -1
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workspace/development-node.d.ts +42 -0
- package/dist/workspace/development-node.d.ts.map +1 -0
- package/dist/workspace/development-node.js +208 -0
- package/dist/workspace/index.d.ts +20 -0
- package/dist/workspace/index.d.ts.map +1 -0
- package/dist/workspace/index.js +16 -0
- package/dist/workspace/workspace-connection.d.ts +174 -0
- package/dist/workspace/workspace-connection.d.ts.map +1 -0
- package/dist/workspace/workspace-connection.js +290 -0
- package/dist/workspace/workspace-identity.d.ts +13 -0
- package/dist/workspace/workspace-identity.d.ts.map +1 -0
- package/dist/workspace/workspace-identity.js +70 -0
- package/dist/workspace/workspace-types.d.ts +82 -0
- package/dist/workspace/workspace-types.d.ts.map +1 -0
- package/dist/workspace/workspace-types.js +7 -0
- package/package.json +5 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-B8U4A1n1.wasm +0 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
|
2
|
+
use sha2::{Digest, Sha256};
|
|
3
|
+
use std::collections::HashMap;
|
|
4
|
+
use std::sync::{Arc, RwLock};
|
|
5
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
6
|
+
|
|
7
|
+
/// Transaction status for idempotency and recovery.
|
|
8
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
9
|
+
#[serde(rename_all = "lowercase")]
|
|
10
|
+
pub enum TransactionStatus {
|
|
11
|
+
/// Transaction status is unknown (may be in-flight or lost).
|
|
12
|
+
Unknown,
|
|
13
|
+
/// Transaction successfully committed.
|
|
14
|
+
Committed,
|
|
15
|
+
/// Transaction was rejected (validation failure).
|
|
16
|
+
Rejected,
|
|
17
|
+
/// Transaction conflicted (concurrent write or version mismatch).
|
|
18
|
+
Conflicted,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Record of a transaction for idempotency and recovery.
|
|
22
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
23
|
+
pub struct TransactionRecord {
|
|
24
|
+
/// Unique transaction ID.
|
|
25
|
+
pub transaction_id: String,
|
|
26
|
+
|
|
27
|
+
/// Current status of the transaction.
|
|
28
|
+
pub status: TransactionStatus,
|
|
29
|
+
|
|
30
|
+
/// Hash of the payload for idempotency checking.
|
|
31
|
+
/// Same ID + same payload = retry is safe
|
|
32
|
+
/// Same ID + different payload = conflict
|
|
33
|
+
pub payload_hash: String,
|
|
34
|
+
|
|
35
|
+
/// State version after transaction (if committed).
|
|
36
|
+
pub state_version: u64,
|
|
37
|
+
|
|
38
|
+
/// When this transaction was recorded.
|
|
39
|
+
pub recorded_at: u64,
|
|
40
|
+
|
|
41
|
+
/// When the transaction was committed (if successful).
|
|
42
|
+
pub committed_at: Option<u64>,
|
|
43
|
+
|
|
44
|
+
/// Reason for rejection or conflict (if applicable).
|
|
45
|
+
pub error: Option<String>,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Request to check transaction status.
|
|
49
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
50
|
+
pub struct TransactionStatusRequest {
|
|
51
|
+
pub transaction_id: String,
|
|
52
|
+
pub payload_hash: Option<String>,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Response with transaction status.
|
|
56
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
57
|
+
pub struct TransactionStatusResponse {
|
|
58
|
+
pub transaction_id: String,
|
|
59
|
+
pub status: TransactionStatus,
|
|
60
|
+
pub payload_hash: String,
|
|
61
|
+
pub state_version: u64,
|
|
62
|
+
pub committed_at: Option<u64>,
|
|
63
|
+
pub error: Option<String>,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Recoverable transaction store.
|
|
67
|
+
#[derive(Clone)]
|
|
68
|
+
pub struct TransactionRecoveryStore {
|
|
69
|
+
records: Arc<RwLock<HashMap<String, TransactionRecord>>>,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
impl TransactionRecoveryStore {
|
|
73
|
+
/// Creates a new recovery store.
|
|
74
|
+
pub fn new() -> Self {
|
|
75
|
+
Self {
|
|
76
|
+
records: Arc::new(RwLock::new(HashMap::new())),
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Records a transaction being processed.
|
|
81
|
+
pub fn record_unknown(
|
|
82
|
+
&self,
|
|
83
|
+
transaction_id: String,
|
|
84
|
+
payload_hash: String,
|
|
85
|
+
) -> Result<(), String> {
|
|
86
|
+
let mut records = self
|
|
87
|
+
.records
|
|
88
|
+
.write()
|
|
89
|
+
.map_err(|_| "store lock poisoned".to_string())?;
|
|
90
|
+
|
|
91
|
+
let id = transaction_id.clone();
|
|
92
|
+
records.insert(
|
|
93
|
+
transaction_id,
|
|
94
|
+
TransactionRecord {
|
|
95
|
+
transaction_id: id,
|
|
96
|
+
status: TransactionStatus::Unknown,
|
|
97
|
+
payload_hash,
|
|
98
|
+
state_version: 0,
|
|
99
|
+
recorded_at: now(),
|
|
100
|
+
committed_at: None,
|
|
101
|
+
error: None,
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
Ok(())
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Records a successful commit.
|
|
109
|
+
pub fn record_committed(
|
|
110
|
+
&self,
|
|
111
|
+
transaction_id: String,
|
|
112
|
+
payload_hash: String,
|
|
113
|
+
state_version: u64,
|
|
114
|
+
) -> Result<(), String> {
|
|
115
|
+
let mut records = self
|
|
116
|
+
.records
|
|
117
|
+
.write()
|
|
118
|
+
.map_err(|_| "store lock poisoned".to_string())?;
|
|
119
|
+
|
|
120
|
+
let txn_id = transaction_id.clone();
|
|
121
|
+
records.insert(
|
|
122
|
+
txn_id.clone(),
|
|
123
|
+
TransactionRecord {
|
|
124
|
+
transaction_id: txn_id,
|
|
125
|
+
status: TransactionStatus::Committed,
|
|
126
|
+
payload_hash,
|
|
127
|
+
state_version,
|
|
128
|
+
recorded_at: now(),
|
|
129
|
+
committed_at: Some(now()),
|
|
130
|
+
error: None,
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
Ok(())
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Records a rejection.
|
|
138
|
+
pub fn record_rejected(
|
|
139
|
+
&self,
|
|
140
|
+
transaction_id: String,
|
|
141
|
+
payload_hash: String,
|
|
142
|
+
error: String,
|
|
143
|
+
) -> Result<(), String> {
|
|
144
|
+
let mut records = self
|
|
145
|
+
.records
|
|
146
|
+
.write()
|
|
147
|
+
.map_err(|_| "store lock poisoned".to_string())?;
|
|
148
|
+
|
|
149
|
+
let id = transaction_id.clone();
|
|
150
|
+
records.insert(
|
|
151
|
+
id.clone(),
|
|
152
|
+
TransactionRecord {
|
|
153
|
+
transaction_id: id,
|
|
154
|
+
status: TransactionStatus::Rejected,
|
|
155
|
+
payload_hash,
|
|
156
|
+
state_version: 0,
|
|
157
|
+
recorded_at: now(),
|
|
158
|
+
committed_at: None,
|
|
159
|
+
error: Some(error),
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
Ok(())
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// Records a conflict.
|
|
167
|
+
pub fn record_conflicted(
|
|
168
|
+
&self,
|
|
169
|
+
transaction_id: String,
|
|
170
|
+
payload_hash: String,
|
|
171
|
+
error: String,
|
|
172
|
+
) -> Result<(), String> {
|
|
173
|
+
let mut records = self
|
|
174
|
+
.records
|
|
175
|
+
.write()
|
|
176
|
+
.map_err(|_| "store lock poisoned".to_string())?;
|
|
177
|
+
|
|
178
|
+
let id = transaction_id.clone();
|
|
179
|
+
records.insert(
|
|
180
|
+
id.clone(),
|
|
181
|
+
TransactionRecord {
|
|
182
|
+
transaction_id: id,
|
|
183
|
+
status: TransactionStatus::Conflicted,
|
|
184
|
+
payload_hash,
|
|
185
|
+
state_version: 0,
|
|
186
|
+
recorded_at: now(),
|
|
187
|
+
committed_at: None,
|
|
188
|
+
error: Some(error),
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
Ok(())
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// Gets transaction status.
|
|
196
|
+
pub fn get_status(&self, transaction_id: &str) -> Result<TransactionStatusResponse, String> {
|
|
197
|
+
let records = self
|
|
198
|
+
.records
|
|
199
|
+
.read()
|
|
200
|
+
.map_err(|_| "store lock poisoned".to_string())?;
|
|
201
|
+
|
|
202
|
+
let record = records
|
|
203
|
+
.get(transaction_id)
|
|
204
|
+
.ok_or(format!("transaction not found: {}", transaction_id))?;
|
|
205
|
+
|
|
206
|
+
Ok(TransactionStatusResponse {
|
|
207
|
+
transaction_id: record.transaction_id.clone(),
|
|
208
|
+
status: record.status,
|
|
209
|
+
payload_hash: record.payload_hash.clone(),
|
|
210
|
+
state_version: record.state_version,
|
|
211
|
+
committed_at: record.committed_at,
|
|
212
|
+
error: record.error.clone(),
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/// Checks if a retry is safe (idempotent).
|
|
217
|
+
pub fn is_retry_safe(&self, transaction_id: &str, payload_hash: &str) -> Result<bool, String> {
|
|
218
|
+
let records = self
|
|
219
|
+
.records
|
|
220
|
+
.read()
|
|
221
|
+
.map_err(|_| "store lock poisoned".to_string())?;
|
|
222
|
+
|
|
223
|
+
let record = records.get(transaction_id);
|
|
224
|
+
|
|
225
|
+
match record {
|
|
226
|
+
None => Ok(true), // Unknown transaction - safe to retry
|
|
227
|
+
Some(r) => {
|
|
228
|
+
// Same payload = safe to retry
|
|
229
|
+
if r.payload_hash == payload_hash {
|
|
230
|
+
Ok(r.status == TransactionStatus::Committed || r.status == TransactionStatus::Unknown)
|
|
231
|
+
} else {
|
|
232
|
+
// Different payload = conflict
|
|
233
|
+
Ok(false)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// Clears old transaction records (for cleanup).
|
|
240
|
+
pub fn cleanup_before(&self, cutoff_time: u64) -> Result<usize, String> {
|
|
241
|
+
let mut records = self
|
|
242
|
+
.records
|
|
243
|
+
.write()
|
|
244
|
+
.map_err(|_| "store lock poisoned".to_string())?;
|
|
245
|
+
|
|
246
|
+
let before_len = records.len();
|
|
247
|
+
records.retain(|_, r| r.recorded_at >= cutoff_time);
|
|
248
|
+
let after_len = records.len();
|
|
249
|
+
|
|
250
|
+
Ok(before_len - after_len)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
impl Default for TransactionRecoveryStore {
|
|
255
|
+
fn default() -> Self {
|
|
256
|
+
Self::new()
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/// Computes payload hash for transaction idempotency.
|
|
261
|
+
pub fn compute_payload_hash(payload: &[u8]) -> String {
|
|
262
|
+
let mut hasher = Sha256::new();
|
|
263
|
+
hasher.update(payload);
|
|
264
|
+
format!("{:x}", hasher.finalize())
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
fn now() -> u64 {
|
|
268
|
+
SystemTime::now()
|
|
269
|
+
.duration_since(UNIX_EPOCH)
|
|
270
|
+
.unwrap_or_default()
|
|
271
|
+
.as_secs()
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
#[cfg(test)]
|
|
275
|
+
mod tests {
|
|
276
|
+
use super::*;
|
|
277
|
+
|
|
278
|
+
fn test_payload_hash() -> String {
|
|
279
|
+
compute_payload_hash(b"test payload")
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
#[test]
|
|
283
|
+
fn record_and_retrieve_committed_transaction() {
|
|
284
|
+
let store = TransactionRecoveryStore::new();
|
|
285
|
+
|
|
286
|
+
store
|
|
287
|
+
.record_committed("txn_123".into(), test_payload_hash(), 42)
|
|
288
|
+
.expect("record failed");
|
|
289
|
+
|
|
290
|
+
let status = store.get_status("txn_123").expect("get failed");
|
|
291
|
+
assert_eq!(status.status, TransactionStatus::Committed);
|
|
292
|
+
assert_eq!(status.state_version, 42);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#[test]
|
|
296
|
+
fn unknown_transaction_not_found() {
|
|
297
|
+
let store = TransactionRecoveryStore::new();
|
|
298
|
+
|
|
299
|
+
let result = store.get_status("txn_unknown");
|
|
300
|
+
assert!(result.is_err());
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
#[test]
|
|
304
|
+
fn same_payload_is_safe_to_retry() {
|
|
305
|
+
let store = TransactionRecoveryStore::new();
|
|
306
|
+
let hash = test_payload_hash();
|
|
307
|
+
|
|
308
|
+
store
|
|
309
|
+
.record_committed("txn_123".into(), hash.clone(), 42)
|
|
310
|
+
.expect("record failed");
|
|
311
|
+
|
|
312
|
+
let is_safe = store.is_retry_safe("txn_123", &hash).expect("check failed");
|
|
313
|
+
assert!(is_safe);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
#[test]
|
|
317
|
+
fn different_payload_is_conflict() {
|
|
318
|
+
let store = TransactionRecoveryStore::new();
|
|
319
|
+
let hash1 = test_payload_hash();
|
|
320
|
+
let hash2 = compute_payload_hash(b"different payload");
|
|
321
|
+
|
|
322
|
+
store
|
|
323
|
+
.record_committed("txn_123".into(), hash1, 42)
|
|
324
|
+
.expect("record failed");
|
|
325
|
+
|
|
326
|
+
let is_safe = store.is_retry_safe("txn_123", &hash2).expect("check failed");
|
|
327
|
+
assert!(!is_safe);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
#[test]
|
|
331
|
+
fn unknown_transaction_is_safe_to_retry() {
|
|
332
|
+
let store = TransactionRecoveryStore::new();
|
|
333
|
+
let hash = test_payload_hash();
|
|
334
|
+
|
|
335
|
+
let is_safe = store.is_retry_safe("txn_unknown", &hash).expect("check failed");
|
|
336
|
+
assert!(is_safe);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
#[test]
|
|
340
|
+
fn rejected_transaction_with_same_payload_is_not_safe() {
|
|
341
|
+
let store = TransactionRecoveryStore::new();
|
|
342
|
+
let hash = test_payload_hash();
|
|
343
|
+
|
|
344
|
+
store
|
|
345
|
+
.record_rejected("txn_123".into(), hash.clone(), "validation error".into())
|
|
346
|
+
.expect("record failed");
|
|
347
|
+
|
|
348
|
+
let is_safe = store.is_retry_safe("txn_123", &hash).expect("check failed");
|
|
349
|
+
assert!(!is_safe);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
#[test]
|
|
353
|
+
fn conflicted_transaction_with_same_payload_is_not_safe() {
|
|
354
|
+
let store = TransactionRecoveryStore::new();
|
|
355
|
+
let hash = test_payload_hash();
|
|
356
|
+
|
|
357
|
+
store
|
|
358
|
+
.record_conflicted("txn_123".into(), hash.clone(), "version mismatch".into())
|
|
359
|
+
.expect("record failed");
|
|
360
|
+
|
|
361
|
+
let is_safe = store.is_retry_safe("txn_123", &hash).expect("check failed");
|
|
362
|
+
assert!(!is_safe);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
#[test]
|
|
366
|
+
fn record_unknown_transaction() {
|
|
367
|
+
let store = TransactionRecoveryStore::new();
|
|
368
|
+
let hash = test_payload_hash();
|
|
369
|
+
|
|
370
|
+
store
|
|
371
|
+
.record_unknown("txn_123".into(), hash)
|
|
372
|
+
.expect("record failed");
|
|
373
|
+
|
|
374
|
+
let status = store.get_status("txn_123").expect("get failed");
|
|
375
|
+
assert_eq!(status.status, TransactionStatus::Unknown);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
#[test]
|
|
379
|
+
fn record_rejected_transaction() {
|
|
380
|
+
let store = TransactionRecoveryStore::new();
|
|
381
|
+
let hash = test_payload_hash();
|
|
382
|
+
|
|
383
|
+
store
|
|
384
|
+
.record_rejected(
|
|
385
|
+
"txn_123".into(),
|
|
386
|
+
hash,
|
|
387
|
+
"required field missing".into(),
|
|
388
|
+
)
|
|
389
|
+
.expect("record failed");
|
|
390
|
+
|
|
391
|
+
let status = store.get_status("txn_123").expect("get failed");
|
|
392
|
+
assert_eq!(status.status, TransactionStatus::Rejected);
|
|
393
|
+
assert!(status.error.is_some());
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#[test]
|
|
397
|
+
fn record_conflicted_transaction() {
|
|
398
|
+
let store = TransactionRecoveryStore::new();
|
|
399
|
+
let hash = test_payload_hash();
|
|
400
|
+
|
|
401
|
+
store
|
|
402
|
+
.record_conflicted("txn_123".into(), hash, "concurrent write".into())
|
|
403
|
+
.expect("record failed");
|
|
404
|
+
|
|
405
|
+
let status = store.get_status("txn_123").expect("get failed");
|
|
406
|
+
assert_eq!(status.status, TransactionStatus::Conflicted);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
#[test]
|
|
410
|
+
fn cleanup_removes_old_records() {
|
|
411
|
+
let store = TransactionRecoveryStore::new();
|
|
412
|
+
let hash = test_payload_hash();
|
|
413
|
+
let old_time = now() - 86400; // 1 day ago
|
|
414
|
+
|
|
415
|
+
store
|
|
416
|
+
.record_committed("txn_old".into(), hash.clone(), 1)
|
|
417
|
+
.expect("record failed");
|
|
418
|
+
|
|
419
|
+
// Manually insert an old record (normally would be done by recovery mechanism)
|
|
420
|
+
{
|
|
421
|
+
let mut records = store.records.write().unwrap();
|
|
422
|
+
if let Some(r) = records.get_mut("txn_old") {
|
|
423
|
+
r.recorded_at = old_time;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
let cutoff = now() - 3600; // 1 hour ago
|
|
428
|
+
let deleted = store.cleanup_before(cutoff).expect("cleanup failed");
|
|
429
|
+
assert_eq!(deleted, 1);
|
|
430
|
+
|
|
431
|
+
// Old record should be gone
|
|
432
|
+
assert!(store.get_status("txn_old").is_err());
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
#[test]
|
|
436
|
+
fn payload_hash_is_deterministic() {
|
|
437
|
+
let hash1 = compute_payload_hash(b"test payload");
|
|
438
|
+
let hash2 = compute_payload_hash(b"test payload");
|
|
439
|
+
assert_eq!(hash1, hash2);
|
|
440
|
+
|
|
441
|
+
let hash3 = compute_payload_hash(b"different");
|
|
442
|
+
assert_ne!(hash1, hash3);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
#[test]
|
|
446
|
+
fn transaction_record_includes_timestamps() {
|
|
447
|
+
let store = TransactionRecoveryStore::new();
|
|
448
|
+
let hash = test_payload_hash();
|
|
449
|
+
|
|
450
|
+
let before = now();
|
|
451
|
+
store
|
|
452
|
+
.record_committed("txn_123".into(), hash, 42)
|
|
453
|
+
.expect("record failed");
|
|
454
|
+
let after = now();
|
|
455
|
+
|
|
456
|
+
let status = store.get_status("txn_123").expect("get failed");
|
|
457
|
+
assert!(status.committed_at.is_some());
|
|
458
|
+
let committed = status.committed_at.unwrap();
|
|
459
|
+
assert!(committed >= before && committed <= after + 1);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# FeltDB Development Workspace
|
|
2
|
+
|
|
3
|
+
This directory contains the development workspace configuration for your FeltDB project.
|
|
4
|
+
|
|
5
|
+
## workspace.json
|
|
6
|
+
|
|
7
|
+
**Purpose:** Durable pairing identity for your project's development workspace.
|
|
8
|
+
|
|
9
|
+
**Contents:**
|
|
10
|
+
- `workspaceId` — Unique identifier for this workspace (format: `ws_*`)
|
|
11
|
+
- `projectId` — Your project name
|
|
12
|
+
- `version` — Configuration version
|
|
13
|
+
|
|
14
|
+
**Usage:**
|
|
15
|
+
- Committed to version control (this is your project's identity)
|
|
16
|
+
- Used by CLI, IDE, agents, and browser extensions to discover the workspace
|
|
17
|
+
- DO NOT edit manually unless you know what you're doing
|
|
18
|
+
|
|
19
|
+
**Discovery:**
|
|
20
|
+
When any FeltDB tool (VS Code, Claude agent, CLI, browser extension) opens your project, it reads this file to automatically connect to the correct workspace.
|
|
21
|
+
|
|
22
|
+
## Runtime Files (not committed)
|
|
23
|
+
|
|
24
|
+
- `pairing.json` — Short-lived pairing token for browser discovery (gitignored)
|
|
25
|
+
- `state.json` — Local workspace state (gitignored)
|
|
26
|
+
- `*.log` — Development logs (gitignored)
|
|
27
|
+
|
|
28
|
+
## Commands
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Check workspace status
|
|
32
|
+
npm run feltdb:status
|
|
33
|
+
|
|
34
|
+
# Or use the CLI directly
|
|
35
|
+
feltdb workspace status
|
|
36
|
+
|
|
37
|
+
# Launch development environment (includes workspace authority)
|
|
38
|
+
npm run dev
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Development Workspace Architecture
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
Your Project
|
|
45
|
+
↓
|
|
46
|
+
.feltdb/workspace.json (pairing identity)
|
|
47
|
+
↓
|
|
48
|
+
feltdb dev (workspace authority)
|
|
49
|
+
↓
|
|
50
|
+
Browser, IDE, Agent clients
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
All tools automatically discover your workspace using `workspace.json` and connect to the local development environment started by `npm run dev`.
|
|
54
|
+
|
|
55
|
+
## Learn More
|
|
56
|
+
|
|
57
|
+
- [Development Workspaces Documentation](https://github.com/rkendel1/feltdb/blob/main/packages/core/docs/development-workspaces.md)
|
|
58
|
+
- [FeltDB Documentation](https://github.com/rkendel1/feltdb)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Initialization
|
|
3
|
+
*
|
|
4
|
+
* Initializes a FeltDB Development Workspace for newly created projects.
|
|
5
|
+
* This is the bootstrap mechanism that enables all FeltDB-aware tools
|
|
6
|
+
* to discover and connect to the same workspace.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
/**
|
|
11
|
+
* Generate a unique workspace ID
|
|
12
|
+
* Format: ws_<projectId>_<timestamp>_<random>
|
|
13
|
+
*/
|
|
14
|
+
export function generateWorkspaceId(projectId) {
|
|
15
|
+
const timestamp = Date.now();
|
|
16
|
+
const random = Math.random().toString(36).substring(2, 9);
|
|
17
|
+
return `ws_${projectId}_${timestamp}_${random}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Initialize development workspace for a new project
|
|
21
|
+
*
|
|
22
|
+
* Creates .feltdb/workspace.json with the workspace discovery information.
|
|
23
|
+
* This file is committed to the repository and used by all development tools
|
|
24
|
+
* (CLI, IDE, agents, browser extensions) to discover and connect to the
|
|
25
|
+
* same workspace.
|
|
26
|
+
*/
|
|
27
|
+
export function initializeWorkspace(projectDir, projectId) {
|
|
28
|
+
const feltdbDir = path.join(projectDir, '.feltdb');
|
|
29
|
+
// Ensure .feltdb directory exists
|
|
30
|
+
if (!fs.existsSync(feltdbDir)) {
|
|
31
|
+
fs.mkdirSync(feltdbDir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
// Create workspace discovery
|
|
34
|
+
const workspaceId = generateWorkspaceId(projectId);
|
|
35
|
+
const discovery = {
|
|
36
|
+
workspaceId,
|
|
37
|
+
projectId,
|
|
38
|
+
version: 1,
|
|
39
|
+
};
|
|
40
|
+
// Write workspace.json
|
|
41
|
+
const workspacePath = path.join(feltdbDir, 'workspace.json');
|
|
42
|
+
fs.writeFileSync(workspacePath, JSON.stringify(discovery, null, 2) + '\n');
|
|
43
|
+
return discovery;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Create .gitignore entry for workspace runtime files
|
|
47
|
+
*
|
|
48
|
+
* The .feltdb/workspace.json is committed (pairing identity).
|
|
49
|
+
* But runtime files like pairing tokens should not be committed.
|
|
50
|
+
*/
|
|
51
|
+
export function appendWorkspaceGitignore(projectDir) {
|
|
52
|
+
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
53
|
+
const workspaceGitignoreEntries = [
|
|
54
|
+
'',
|
|
55
|
+
'# FeltDB Development Workspace',
|
|
56
|
+
'.feltdb/pairing.json',
|
|
57
|
+
'.feltdb/state.json',
|
|
58
|
+
'.feltdb/*.log',
|
|
59
|
+
'',
|
|
60
|
+
].join('\n');
|
|
61
|
+
if (fs.existsSync(gitignorePath)) {
|
|
62
|
+
const existing = fs.readFileSync(gitignorePath, 'utf-8');
|
|
63
|
+
if (!existing.includes('.feltdb/pairing.json')) {
|
|
64
|
+
fs.appendFileSync(gitignorePath, workspaceGitignoreEntries);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const content = [
|
|
69
|
+
'node_modules/',
|
|
70
|
+
'dist/',
|
|
71
|
+
'build/',
|
|
72
|
+
'.env.local',
|
|
73
|
+
workspaceGitignoreEntries,
|
|
74
|
+
].join('\n');
|
|
75
|
+
fs.writeFileSync(gitignorePath, content);
|
|
76
|
+
}
|
|
77
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -65,4 +65,10 @@ export type { TelemetryEvent, TelemetryEventType } from './telemetry.js';
|
|
|
65
65
|
* Observability and inspection APIs
|
|
66
66
|
*/
|
|
67
67
|
export type { ProvenanceGraph, ProvenanceNode, ProvenanceEdge, ProvenanceEdgeType, RuntimeDiagnostics, } from './db.js';
|
|
68
|
+
/**
|
|
69
|
+
* Development Workspace APIs
|
|
70
|
+
*
|
|
71
|
+
* Shared development state for coordinating Browser, IDE, and Agent clients.
|
|
72
|
+
*/
|
|
73
|
+
export * from './workspace/index.js';
|
|
68
74
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,cAAc,SAAS,CAAC;AACxB,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,2BAA2B,CAAC;AAC1C,OAAO,EACL,eAAe,EACf,KAAK,OAAO,EACZ,KAAK,SAAS,IAAI,iBAAiB,EACnC,KAAK,QAAQ,IAAI,gBAAgB,GAClC,MAAM,eAAe,CAAC;AAEvB;;GAEG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AAEnC;;GAEG;AACH,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACnE,YAAY,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEzE;;GAEG;AACH,YAAY,EACV,eAAe,EACf,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,cAAc,SAAS,CAAC;AACxB,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,2BAA2B,CAAC;AAC1C,OAAO,EACL,eAAe,EACf,KAAK,OAAO,EACZ,KAAK,SAAS,IAAI,iBAAiB,EACnC,KAAK,QAAQ,IAAI,gBAAgB,GAClC,MAAM,eAAe,CAAC;AAEvB;;GAEG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AAEnC;;GAEG;AACH,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACnE,YAAY,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEzE;;GAEG;AACH,YAAY,EACV,eAAe,EACf,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,SAAS,CAAC;AAEjB;;;;GAIG;AACH,cAAc,sBAAsB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -60,3 +60,9 @@ export * from './agent-runtime.js';
|
|
|
60
60
|
* Telemetry APIs
|
|
61
61
|
*/
|
|
62
62
|
export { emitTelemetry, getTelemetryClient } from './telemetry.js';
|
|
63
|
+
/**
|
|
64
|
+
* Development Workspace APIs
|
|
65
|
+
*
|
|
66
|
+
* Shared development state for coordinating Browser, IDE, and Agent clients.
|
|
67
|
+
*/
|
|
68
|
+
export * from './workspace/index.js';
|