@feltdb/core 0.10.0 → 0.11.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.
@@ -9,7 +9,7 @@ import { createRequire } from 'node:module';
9
9
  import { spawn, spawnSync } from 'child_process';
10
10
  import { randomBytes } from 'crypto';
11
11
  import { createInterface } from 'node:readline/promises';
12
- import { createFeltDB, diffFlowSpec, formatFlowSpec, InvestigationLifecycleManager, listFeltDBModules, parseFlowSpec, planFlowSpecMigration, renderConvergenceDiagnostic, renderProposalDiagnostic, resolveFeltDBModule, resolveModuleIntent, startLocalDevelopmentAuthority, validateFlowSpec, verifyProposalConvergence } from '@feltdb/core';
12
+ import { createFeltDB, diffFlowSpec, emptyFlowSpec, formatFlowSpec, InvestigationLifecycleManager, listFeltDBModules, parseFlowSpec, planFlowSpecMigration, renderConvergenceDiagnostic, renderProposalDiagnostic, resolveFeltDBModule, resolveModuleIntent, startLocalDevelopmentAuthority, validateFlowSpec, verifyProposalConvergence } from '@feltdb/core';
13
13
  import { collectApplicationUnderstanding } from '@feltdb/core';
14
14
  import { collectRepositoryEvidence } from './repository-evidence.js';
15
15
  import { discoverWorkspace, ensureWorkspaceGitIgnored, generatePairingToken, persistPairingToken, displayWorkspaceStatus, initializeWorkspace, startPairingDiscoveryServer } from './workspace-integration.js';
@@ -24,6 +24,7 @@ import { proposalService } from './proposal-client.js';
24
24
  import { resolveManagedAuthorityEnvironment } from './managed-environment.js';
25
25
  import { readManagedProjectState } from './managed-project.js';
26
26
  import { resolveCanonicalStudioArtifact } from './studio-artifact.js';
27
+ import { configureManagedAccount, finalizeManagedProvisioning } from '../create/index.js';
27
28
  function loadProjectEnvironment(file = path.resolve('.env.local')) {
28
29
  if (!fs.existsSync(file))
29
30
  return;
@@ -61,6 +62,8 @@ export async function handleCommand(command, args) {
61
62
  return handleServer(args);
62
63
  case 'keys':
63
64
  return handleKeys(args);
65
+ case 'init-managed':
66
+ return handleInitManaged(args);
64
67
  case 'connect':
65
68
  return handleConnect(args);
66
69
  case 'status':
@@ -864,6 +867,155 @@ async function handleConnect(args) {
864
867
  console.log(`Health: ${health.status || 'healthy'}\n`);
865
868
  console.log('Use: feltdb status (to check connection)\n');
866
869
  }
870
+ async function promptSecret(message) {
871
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
872
+ return '';
873
+ const input = process.stdin;
874
+ const output = process.stdout;
875
+ const wasRaw = input.isRaw;
876
+ const readline = await import('node:readline');
877
+ readline.emitKeypressEvents(input);
878
+ input.setRawMode(true);
879
+ input.resume();
880
+ output.write(message);
881
+ return new Promise(resolve => {
882
+ let value = '';
883
+ const finish = () => {
884
+ input.off('keypress', onKeypress);
885
+ input.setRawMode(Boolean(wasRaw));
886
+ input.pause();
887
+ output.write('\n');
888
+ resolve(value);
889
+ };
890
+ const onKeypress = (character, key) => {
891
+ if (key.ctrl && key.name === 'c')
892
+ process.exit(130);
893
+ if (key.name === 'return' || key.name === 'enter')
894
+ return finish();
895
+ if (key.name === 'backspace') {
896
+ value = value.slice(0, -1);
897
+ return;
898
+ }
899
+ if (!key.ctrl && !key.meta && character)
900
+ value += character;
901
+ };
902
+ input.on('keypress', onKeypress);
903
+ });
904
+ }
905
+ export function ensureManagedProjectContract(projectDir) {
906
+ const projectName = path.basename(projectDir);
907
+ const flowApp = projectName.replace(/[^A-Za-z0-9_]/g, '_').replace(/^[^A-Za-z_]/, '_$&');
908
+ const configPath = path.join(projectDir, 'feltdb.config.json');
909
+ const flowPath = path.join(projectDir, 'feltdb.flow');
910
+ const created = { config: false, flow: false };
911
+ if (!fs.existsSync(configPath)) {
912
+ fs.writeFileSync(configPath, `${JSON.stringify({
913
+ namespace: projectName,
914
+ mode: 'remote',
915
+ runtime: 'managed',
916
+ storage: 'managed',
917
+ application: { lifecycle: 'managed' },
918
+ }, null, 2)}\n`);
919
+ created.config = true;
920
+ }
921
+ if (!fs.existsSync(flowPath)) {
922
+ const draft = formatFlowSpec(emptyFlowSpec(flowApp)).replace(/\n\napp /, '\n\n// Recovery draft: define the application contract before publishing.\napp ');
923
+ fs.writeFileSync(flowPath, draft);
924
+ created.flow = true;
925
+ }
926
+ return { ...created, configPath, flowPath };
927
+ }
928
+ async function handleInitManaged(args) {
929
+ loadProjectEnvironment();
930
+ const projectDir = process.cwd();
931
+ const initialized = ensureManagedProjectContract(projectDir);
932
+ if (initialized.config)
933
+ console.log('✓ Created feltdb.config.json');
934
+ if (initialized.flow) {
935
+ console.log('⚠️ feltdb.flow was missing; created an empty recovery draft.');
936
+ console.log(' FeltDB cannot infer the application authority from source code.');
937
+ console.log(' Review and complete feltdb.flow before publishing.\n');
938
+ }
939
+ // Check if already managed
940
+ const managedState = readManagedProjectState(projectDir);
941
+ if (managedState.applicationId) {
942
+ console.log('✅ This project is already configured for managed FeltDB\n');
943
+ console.log(`Application: ${managedState.applicationId}`);
944
+ console.log(`Namespace: ${managedState.namespace}`);
945
+ console.log(`URL: ${managedState.url}`);
946
+ if (managedState.revisionId) {
947
+ console.log(`Revision: ${managedState.revisionId}`);
948
+ }
949
+ console.log('\nTo publish the local authority contract, run:');
950
+ console.log(' feltdb publish --approve-destructive-changes --yes\n');
951
+ return;
952
+ }
953
+ // Prompt for credentials
954
+ console.log('☁️ Setting up managed FeltDB for this project\n');
955
+ const prompt = await import('node:readline/promises').then(m => m.createInterface({ input: process.stdin, output: process.stdout }));
956
+ const email = args[0] || await prompt.question('Email: ');
957
+ if (!email)
958
+ throw new Error('Email is required');
959
+ const password = args[1] || await promptSecret('Password (8+ characters): ');
960
+ if (password.length < 8)
961
+ throw new Error('Password must be at least 8 characters');
962
+ prompt.close();
963
+ console.log('\nProvisioning managed account...\n');
964
+ try {
965
+ const projectConfig = JSON.parse(fs.readFileSync(initialized.configPath, 'utf8'));
966
+ const namespace = projectConfig.namespace || path.basename(projectDir);
967
+ const applicationName = projectConfig.name || path.basename(projectDir);
968
+ const managed = await configureManagedAccount({
969
+ projectDir,
970
+ applicationName,
971
+ namespace,
972
+ email,
973
+ password,
974
+ });
975
+ console.log(`✓ Managed application: ${managed.application.name}`);
976
+ console.log(`✓ Namespace: ${managed.application.namespace || namespace}`);
977
+ console.log(`✓ Environment configured in .env.local\n`);
978
+ if (initialized.flow) {
979
+ console.log('✅ Managed connection setup complete.');
980
+ console.log('Automatic publication was skipped because feltdb.flow had to be recovered.');
981
+ console.log('Review feltdb.flow, then run:');
982
+ console.log(' feltdb validate');
983
+ console.log(' feltdb publish --approve-destructive-changes --yes\n');
984
+ return;
985
+ }
986
+ // Ask if user wants to publish the initial revision
987
+ const publishPrompt = await import('node:readline/promises').then(m => m.createInterface({ input: process.stdin, output: process.stdout }));
988
+ const shouldPublish = await publishPrompt.question('Publish initial application revision now? [Y/n] ');
989
+ publishPrompt.close();
990
+ if (!/^(n|no)$/i.test(shouldPublish.trim())) {
991
+ console.log('\n🔨 Building and publishing initial revision...\n');
992
+ await publishApplication({
993
+ cwd: projectDir,
994
+ yes: true,
995
+ approveDestructive: true,
996
+ });
997
+ console.log('\n✓ Initial revision published\n');
998
+ // Finalize and verify
999
+ try {
1000
+ const finalized = await finalizeManagedProvisioning({ projectDir });
1001
+ console.log(`✓ Managed runtime verified at revision ${finalized.revisionId}\n`);
1002
+ }
1003
+ catch (error) {
1004
+ console.log('⚠️ Revision published but verification skipped.\n');
1005
+ console.log('Your application is ready to use. Run: npm run dev\n');
1006
+ }
1007
+ }
1008
+ else {
1009
+ console.log('\n✅ Managed setup complete!\n');
1010
+ console.log('Next steps:');
1011
+ console.log(' 1. feltdb publish --approve-destructive-changes --yes (to publish your initial revision)');
1012
+ console.log(' 2. npm run dev (to start development)\n');
1013
+ }
1014
+ }
1015
+ catch (error) {
1016
+ throw error;
1017
+ }
1018
+ }
867
1019
  async function handleExplain(args) {
868
1020
  const flowRef = args[0];
869
1021
  if (!flowRef || !flowRef.startsWith('flow://')) {
@@ -2017,6 +2169,7 @@ Commands:
2017
2169
  diff [file] Diff FlowSpec against the last deployed model
2018
2170
  deploy [file] Validate, version, and deploy FlowSpec
2019
2171
  publish [file] Build and publish/update a managed application
2172
+ init-managed Set up managed FeltDB for an existing project
2020
2173
  ai Open the canonical interactive local-AI workflow
2021
2174
  ai --dry-run Open an ephemeral preview-only AI session
2022
2175
  ai model show Show the selected local or user-provided model
@@ -2066,7 +2219,15 @@ API Key Commands:
2066
2219
  feltdb keys revoke <key-id>
2067
2220
 
2068
2221
  Connection:
2069
- feltdb connect <url>
2222
+ feltdb connect <url> Connect to a running FeltDB server
2223
+
2224
+ Managed FeltDB Setup (Existing Projects):
2225
+ feltdb init-managed Set up managed FeltDB for an existing project
2226
+ Prompts for email and password, provisions resources,
2227
+ and optionally publishes the initial revision
2228
+ feltdb init-managed <email> <password> Non-interactive setup with provided credentials
2229
+
2230
+ Development:
2070
2231
  dev Start development server
2071
2232
  build Build FeltDB application
2072
2233
  check Validate configuration
@@ -2100,6 +2261,8 @@ Examples:
2100
2261
  feltdb studio --connect http://localhost:7700
2101
2262
  feltdb server --port 7700 --data ./data
2102
2263
  feltdb keys create --name development
2264
+ feltdb init-managed (interactive - prompts for email/password)
2265
+ feltdb init-managed user@example.com secret123
2103
2266
  feltdb connect http://localhost:7700
2104
2267
  feltdb status
2105
2268
  feltdb dev
package/dist/cli/index.js CHANGED
@@ -23,7 +23,7 @@ import * as path from 'path';
23
23
  import * as readline from 'readline';
24
24
  import { getClient } from './api-client.js';
25
25
  import { loadFeltDBConfig, createDefaultConfig, validateModel, } from './config.js';
26
- const VERSION = '0.8.0';
26
+ const VERSION = '0.9.0';
27
27
  function prompt(question) {
28
28
  const rl = readline.createInterface({
29
29
  input: process.stdin,
@@ -1 +1 @@
1
- {"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"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"}
@@ -484,10 +484,12 @@ export class Collection {
484
484
  conflictCode: casResult.conflictCode,
485
485
  };
486
486
  }
487
- // Use the item returned by cas() which has the correct incremented version
488
- const finalItem = (casResult.item || updated);
489
- // CAS operation on backend increments version atomically - update finalItem to match
490
- finalItem.__version = casResult.currentVersion;
487
+ // Use the item returned by cas() if available (file-db, memory-db), otherwise use local copy
488
+ // When cas() returns the item, it's the authoritative post-commit state from the backend.
489
+ // When it doesn't, we reconstruct from our local updated object and the returned version.
490
+ const finalItem = casResult.item
491
+ ? casResult.item
492
+ : { ...updated, __version: casResult.currentVersion };
491
493
  // Update indexes
492
494
  this.indexBackend.updateRecord(String(id), current, finalItem);
493
495
  // Emit change through reactive dependency graph
@@ -1,4 +1,4 @@
1
1
  // One release train keeps generated applications installable. The repository
2
2
  // validation script checks these values against every workspace manifest.
3
- export const FELTDB_PACKAGE_VERSION = '0.10.0';
3
+ export const FELTDB_PACKAGE_VERSION = '0.11.1';
4
4
  export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;
@@ -287,12 +287,12 @@ use std::any::type_name;
287
287
  use std::collections::hash_map::DefaultHasher;
288
288
  use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
289
289
  use std::fmt::{Display, Formatter};
290
- use std::fs::{self, OpenOptions};
290
+ use std::fs::{self, File, OpenOptions};
291
291
  use std::hash::{Hash, Hasher};
292
292
  use std::io::{BufRead, BufReader, Write};
293
293
  use std::ops::Bound::{Excluded, Unbounded};
294
294
  use std::path::{Path, PathBuf};
295
- use std::sync::{Arc, Mutex};
295
+ use std::sync::{Arc, Mutex, OnceLock, Weak};
296
296
  use std::time::{SystemTime, UNIX_EPOCH};
297
297
  use tokio::sync::broadcast;
298
298
 
@@ -369,8 +369,17 @@ impl From<String> for FlowError {
369
369
  pub struct FeltDb {
370
370
  inner: Arc<Mutex<Inner>>,
371
371
  event_tx: broadcast::Sender<ChangeEvent>,
372
+ _ownership: Arc<File>,
372
373
  }
373
374
 
375
+ struct OpenDatabase {
376
+ inner: Weak<Mutex<Inner>>,
377
+ event_tx: broadcast::Sender<ChangeEvent>,
378
+ ownership: Weak<File>,
379
+ }
380
+
381
+ static OPEN_DATABASES: OnceLock<Mutex<HashMap<PathBuf, OpenDatabase>>> = OnceLock::new();
382
+
374
383
  #[derive(Default)]
375
384
  #[allow(dead_code)] // Reserved runtime subsystems are initialized before their public APIs land.
376
385
  struct Inner {
@@ -1968,7 +1977,56 @@ struct RetentionRecord {
1968
1977
 
1969
1978
  impl FeltDb {
1970
1979
  pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
1971
- let path = path.as_ref().to_path_buf();
1980
+ let requested_path = path.as_ref();
1981
+ let parent = requested_path
1982
+ .parent()
1983
+ .filter(|parent| !parent.as_os_str().is_empty())
1984
+ .unwrap_or_else(|| Path::new("."));
1985
+ fs::create_dir_all(parent)?;
1986
+ let path = if requested_path.exists() {
1987
+ fs::canonicalize(requested_path)?
1988
+ } else {
1989
+ let file_name = requested_path.file_name().ok_or_else(|| {
1990
+ std::io::Error::new(
1991
+ std::io::ErrorKind::InvalidInput,
1992
+ "FeltDB database path has no file name",
1993
+ )
1994
+ })?;
1995
+ fs::canonicalize(parent)?.join(file_name)
1996
+ };
1997
+
1998
+ let databases = OPEN_DATABASES.get_or_init(|| Mutex::new(HashMap::new()));
1999
+ let mut databases = databases.lock().expect("open database registry poisoned");
2000
+ if let Some(open) = databases.get(&path) {
2001
+ if let (Some(inner), Some(ownership)) =
2002
+ (open.inner.upgrade(), open.ownership.upgrade())
2003
+ {
2004
+ return Ok(Self {
2005
+ inner,
2006
+ event_tx: open.event_tx.clone(),
2007
+ _ownership: ownership,
2008
+ });
2009
+ }
2010
+ }
2011
+ databases.remove(&path);
2012
+
2013
+ let mut ownership_path = path.as_os_str().to_os_string();
2014
+ ownership_path.push(".lock");
2015
+ let ownership = OpenOptions::new()
2016
+ .create(true)
2017
+ .read(true)
2018
+ .write(true)
2019
+ .open(PathBuf::from(ownership_path))?;
2020
+ ownership.try_lock().map_err(|error| match error {
2021
+ std::fs::TryLockError::WouldBlock => std::io::Error::new(
2022
+ std::io::ErrorKind::WouldBlock,
2023
+ format!(
2024
+ "FeltDB database {} is already owned by another process",
2025
+ path.display()
2026
+ ),
2027
+ ),
2028
+ std::fs::TryLockError::Error(error) => error,
2029
+ })?;
1972
2030
 
1973
2031
  // Fail closed. The format is decided before any record is interpreted
1974
2032
  // and before anything is written, so a database this build does not
@@ -1981,9 +2039,6 @@ impl FeltDb {
1981
2039
  FormatCompatibility::Compatible(format) => format,
1982
2040
  };
1983
2041
 
1984
- if let Some(parent) = path.parent() {
1985
- fs::create_dir_all(parent)?;
1986
- }
1987
2042
  if !path.exists() {
1988
2043
  let mut file = OpenOptions::new().create(true).append(true).open(&path)?;
1989
2044
  // A new database states its format in its first record, so it never
@@ -2019,10 +2074,20 @@ impl FeltDb {
2019
2074
 
2020
2075
  let (event_tx, _) = broadcast::channel(1024);
2021
2076
 
2022
- Ok(Self {
2077
+ let database = Self {
2023
2078
  inner: Arc::new(Mutex::new(inner)),
2024
2079
  event_tx,
2025
- })
2080
+ _ownership: Arc::new(ownership),
2081
+ };
2082
+ databases.insert(
2083
+ path,
2084
+ OpenDatabase {
2085
+ inner: Arc::downgrade(&database.inner),
2086
+ event_tx: database.event_tx.clone(),
2087
+ ownership: Arc::downgrade(&database._ownership),
2088
+ },
2089
+ );
2090
+ Ok(database)
2026
2091
  }
2027
2092
 
2028
2093
  /// Write a self-contained durable backup artifact.
@@ -2264,10 +2329,21 @@ impl FeltDb {
2264
2329
  .map(|(cap, _)| cap)
2265
2330
  .unwrap_or("default")
2266
2331
  .to_string();
2267
- self.insert_internal(capability, key.to_string(), value)?;
2332
+ self.insert_internal(capability, key.to_string(), value, false)?;
2268
2333
  Ok(())
2269
2334
  }
2270
2335
 
2336
+ /// Insert only if the logical record is absent, deciding presence under the
2337
+ /// same lock that appends the operation and mints its first revision.
2338
+ pub fn insert_if_absent<T: Serialize>(&self, key: &str, value: T) -> Result<()> {
2339
+ let capability = key
2340
+ .split_once(':')
2341
+ .map(|(cap, _)| cap)
2342
+ .unwrap_or("default")
2343
+ .to_string();
2344
+ self.insert_internal(capability, key.to_string(), value, true)
2345
+ }
2346
+
2271
2347
  /// Update an existing record with vector clock tracking
2272
2348
  pub fn update<T: Serialize>(&self, key: &str, value: T) -> Result<()> {
2273
2349
  let capability = key
@@ -2480,7 +2556,7 @@ impl FeltDb {
2480
2556
  }
2481
2557
 
2482
2558
  // Authorized: apply mutation
2483
- self.insert_internal(capability, key.to_string(), value)?;
2559
+ self.insert_internal(capability, key.to_string(), value, false)?;
2484
2560
  Ok(())
2485
2561
  }
2486
2562
 
@@ -3195,6 +3271,7 @@ impl FeltDb {
3195
3271
  capability: String,
3196
3272
  key: String,
3197
3273
  value: T,
3274
+ require_absent: bool,
3198
3275
  ) -> Result<()> {
3199
3276
  let rust_type = type_name::<T>().to_string();
3200
3277
  let value_json = serde_json::to_value(value)?;
@@ -3216,6 +3293,21 @@ impl FeltDb {
3216
3293
  {
3217
3294
  let mut inner = self.state();
3218
3295
 
3296
+ if require_absent
3297
+ && inner
3298
+ .rows
3299
+ .get(&capability)
3300
+ .and_then(|bucket| bucket.get(&key))
3301
+ .is_some()
3302
+ {
3303
+ return Err(FlowError::PreconditionFailed(Box::new(
3304
+ PreconditionFailure::Present {
3305
+ collection: capability,
3306
+ key,
3307
+ },
3308
+ )));
3309
+ }
3310
+
3219
3311
  // Increment sequence number
3220
3312
  inner.sequence += 1;
3221
3313
 
@@ -4497,7 +4589,7 @@ impl Capability {
4497
4589
  key
4498
4590
  };
4499
4591
  self.db
4500
- .insert_internal(self.name.clone(), key.clone(), value)?;
4592
+ .insert_internal(self.name.clone(), key.clone(), value, false)?;
4501
4593
  Ok(key)
4502
4594
  }
4503
4595
 
@@ -4507,7 +4599,8 @@ impl Capability {
4507
4599
  } else {
4508
4600
  format!("{}:{key}", self.name)
4509
4601
  };
4510
- self.db.insert_internal(self.name.clone(), full_key, value)
4602
+ self.db
4603
+ .insert_internal(self.name.clone(), full_key, value, false)
4511
4604
  }
4512
4605
 
4513
4606
  pub fn update<T: Serialize>(&self, key: &str, value: T) -> Result<()> {
@@ -0,0 +1,69 @@
1
+ use std::process::Command;
2
+
3
+ use feltdb::{AtomicMutation, FeltDb};
4
+ use serde_json::json;
5
+ use tempfile::TempDir;
6
+
7
+ const CHILD_ENV: &str = "FELTDB_OWNERSHIP_CHILD";
8
+ const PATH_ENV: &str = "FELTDB_OWNERSHIP_PATH";
9
+ const EXPECT_LOCKED_ENV: &str = "FELTDB_EXPECT_LOCKED";
10
+
11
+ #[test]
12
+ fn ownership_child() {
13
+ if std::env::var_os(CHILD_ENV).is_none() {
14
+ return;
15
+ }
16
+ let path = std::env::var(PATH_ENV).unwrap();
17
+ let expect_locked = std::env::var_os(EXPECT_LOCKED_ENV).is_some();
18
+ match FeltDb::open(path) {
19
+ Ok(database) => {
20
+ assert!(!expect_locked, "a second owner opened the database");
21
+ let mutation = AtomicMutation {
22
+ capability: "probe".into(),
23
+ key: "probe:child".into(),
24
+ value: Some(json!({ "owner": "child" })),
25
+ };
26
+ database
27
+ .apply_atomic_transaction("child", None, &[], &[mutation], None)
28
+ .unwrap();
29
+ }
30
+ Err(error) => {
31
+ assert!(expect_locked, "an unowned database failed to open: {error}");
32
+ assert!(error.to_string().contains("already owned"));
33
+ }
34
+ }
35
+ }
36
+
37
+ fn child(path: &std::path::Path, expect_locked: bool) -> std::process::ExitStatus {
38
+ let mut command = Command::new(std::env::current_exe().unwrap());
39
+ command
40
+ .arg("--exact")
41
+ .arg("ownership_child")
42
+ .arg("--nocapture")
43
+ .env(CHILD_ENV, "1")
44
+ .env(PATH_ENV, path);
45
+ if expect_locked {
46
+ command.env(EXPECT_LOCKED_ENV, "1");
47
+ }
48
+ command.status().unwrap()
49
+ }
50
+
51
+ #[test]
52
+ fn another_process_cannot_read_write_or_transact_while_owned() {
53
+ let directory = TempDir::new().unwrap();
54
+ let path = directory.path().join("shared.db");
55
+ let _owner = FeltDb::open(&path).unwrap();
56
+ assert!(child(&path, true).success());
57
+ }
58
+
59
+ #[test]
60
+ fn ownership_is_released_when_the_process_exits() {
61
+ let directory = TempDir::new().unwrap();
62
+ let path = directory.path().join("shared.db");
63
+ assert!(child(&path, false).success());
64
+ let database = FeltDb::open(path).unwrap();
65
+ assert_eq!(
66
+ database.get::<serde_json::Value>("probe:child").unwrap(),
67
+ Some(json!({ "owner": "child" }))
68
+ );
69
+ }
@@ -203,7 +203,7 @@ fn each_compaction_rewrites_the_whole_revision_history() {
203
203
  /// The compacted log is linear in both the revision count and the state size:
204
204
  ///
205
205
  /// ```text
206
- /// bytes = header + count × (fixed_overhead + 2 × |content|)
206
+ /// bytes ≈ header + count × (fixed_overhead + 2 × |content|)
207
207
  /// ```
208
208
  ///
209
209
  /// The fixed part is not incidental — it is a 64-character hex identity, a
@@ -218,10 +218,11 @@ fn each_compaction_rewrites_the_whole_revision_history() {
218
218
  /// design.
219
219
  #[test]
220
220
  fn a_revision_costs_a_fixed_overhead_plus_one_copy_of_its_state() {
221
- let small_ten = compacted_log_after_revisions(10, 0).len();
221
+ let small_ten_log = compacted_log_after_revisions(10, 0);
222
+ let small_ten = small_ten_log.len();
222
223
  let small_twenty = compacted_log_after_revisions(20, 0).len();
223
- let padded_hundred = compacted_log_after_revisions(10, 100).len();
224
- let padded_thousand = compacted_log_after_revisions(10, 1000).len();
224
+ let padded_hundred_log = compacted_log_after_revisions(10, 100);
225
+ let padded_thousand_log = compacted_log_after_revisions(10, 1000);
225
226
 
226
227
  // Ten more revisions of a 16-byte state cost ten more per-revision records.
227
228
  // Indices 10..19 are one digit wider than 0..9, hence the ten-byte term.
@@ -239,17 +240,31 @@ fn a_revision_costs_a_fixed_overhead_plus_one_copy_of_its_state() {
239
240
  );
240
241
 
241
242
  // Content is represented in both the revision and its canonical
242
- // transaction attribution: growing each state by N bytes grows the log by
243
- // exactly count × 2N.
243
+ // transaction attribution. Count those copies directly instead of
244
+ // requiring unrelated encoded metadata to have byte-for-byte identical
245
+ // size across platforms and runs.
244
246
  assert_eq!(
245
- padded_hundred - small_ten,
246
- 10 * 2 * 100,
247
- "state appears in the revision and canonical transaction attribution"
247
+ padded_hundred_log.matches("x".repeat(100).as_str()).count(),
248
+ 10 * 2,
249
+ "each state appears in its revision and canonical transaction attribution"
248
250
  );
249
251
  assert_eq!(
250
- padded_thousand - small_ten,
251
- 10 * 2 * 1000,
252
- "the relationship stays exactly linear at a kilobyte of state"
252
+ padded_thousand_log
253
+ .matches("x".repeat(1000).as_str())
254
+ .count(),
255
+ 10 * 2,
256
+ "the two-copy relationship remains direct at a kilobyte of state"
257
+ );
258
+
259
+ let hundred_growth = padded_hundred_log.len() - small_ten;
260
+ let thousand_growth = padded_thousand_log.len() - small_ten;
261
+ assert!(
262
+ hundred_growth.abs_diff(10 * 2 * 100) <= 100,
263
+ "the measured 100-byte content term was {hundred_growth} bytes"
264
+ );
265
+ assert!(
266
+ thousand_growth.abs_diff(10 * 2 * 1000) <= 1_000,
267
+ "the measured 1,000-byte content term was {thousand_growth} bytes"
253
268
  );
254
269
  }
255
270
 
@@ -13536,17 +13536,19 @@ async fn create_record(
13536
13536
  .take()
13537
13537
  .unwrap_or_else(|| state.ids.fetch_add(1, Ordering::Relaxed).to_string());
13538
13538
  let key = record_key(&collection, &id)?;
13539
- if state.db.get_value(&key)?.is_some() {
13540
- return Err(ApiError(
13541
- StatusCode::CONFLICT,
13542
- "record already exists".to_string(),
13543
- ));
13544
- }
13545
13539
  record
13546
13540
  .value
13547
13541
  .insert("id".to_string(), Value::String(id.clone()));
13548
13542
  let value = Value::Object(record.value);
13549
- state.db.insert(&key, &value)?;
13543
+ state
13544
+ .db
13545
+ .insert_if_absent(&key, &value)
13546
+ .map_err(|error| match error {
13547
+ FlowError::PreconditionFailed(_) => {
13548
+ ApiError(StatusCode::CONFLICT, "record already exists".to_string())
13549
+ }
13550
+ other => ApiError(StatusCode::CONFLICT, other.to_string()),
13551
+ })?;
13550
13552
  Ok((
13551
13553
  StatusCode::CREATED,
13552
13554
  Json(RecordResponse {
@@ -14700,7 +14702,7 @@ async fn control_plane_record_provenance(
14700
14702
  "attribution": {
14701
14703
  "actor": transactions.first().and_then(|attribution| attribution.subject.clone()),
14702
14704
  "authority": row.operation.as_ref().map(|operation| operation.instance_id.clone()),
14703
- "source": if transactions.first().is_some() { "transaction" } else if row.operation.is_some() { "authority" } else { "unknown" },
14705
+ "source": if capability == collection && row.operation.is_some() { "authority" } else if transactions.first().is_some() { "transaction" } else if row.operation.is_some() { "authority" } else { "unknown" },
14704
14706
  },
14705
14707
  })))
14706
14708
  }
package/dist/file-db.d.ts CHANGED
@@ -29,6 +29,7 @@ export declare class FileJsDb implements JsDb {
29
29
  private readonly applied;
30
30
  constructor(path: string);
31
31
  private loadState;
32
+ private loadStateFromBackup;
32
33
  private initializeState;
33
34
  private restorePeers;
34
35
  private isStalelock;
@@ -112,6 +113,7 @@ export declare class FileJsDb implements JsDb {
112
113
  currentVersion: number;
113
114
  currentEpoch?: number;
114
115
  conflictCode?: string;
116
+ item?: any;
115
117
  }>;
116
118
  close(): void;
117
119
  }