Skip to main content

agentmux_srv/backend/storage/filestore/
ijson.rs

1#![allow(dead_code)]
2// Copyright 2025, Command Line Inc.
3// Copyright 2025-2026, AgentMux Corp.
4// SPDX-License-Identifier: Apache-2.0
5
6//! FileStore IJson (incremental JSON) operations.
7
8
9use super::core::FileStore;
10use super::types::FileMeta;
11use crate::backend::storage::error::StoreError;
12
13impl FileStore {
14    /// IJson metadata key: number of commands since last compaction.
15    const IJSON_NUM_COMMANDS: &'static str = "ijson:numcmds";
16    /// IJson metadata key: incremental bytes since last compaction.
17    const IJSON_INC_BYTES: &'static str = "ijson:incbytes";
18
19    /// Compaction threshold: high command count.
20    const IJSON_HIGH_COMMANDS: i64 = 100;
21    /// Compaction threshold: high ratio (incremental/file >= 3x).
22    const IJSON_HIGH_RATIO: f64 = 3.0;
23    /// Compaction threshold: low command count.
24    const IJSON_LOW_COMMANDS: i64 = 10;
25    /// Compaction threshold: low ratio (incremental/file >= 1x).
26    const IJSON_LOW_RATIO: f64 = 1.0;
27
28    /// Append an IJson command to a file. Triggers compaction if thresholds are exceeded.
29    #[track_caller]
30    pub fn append_ijson(
31        &self,
32        zone_id: &str,
33        name: &str,
34        command: &serde_json::Value,
35    ) -> Result<(), StoreError> {
36        let file = self.stat(zone_id, name)?.ok_or(StoreError::NotFound)?;
37        if !file.opts.ijson {
38            return Err(StoreError::Other("file is not ijson".to_string()));
39        }
40
41        let cmd_bytes = serde_json::to_string(command)?;
42        let data = format!("{}\n", cmd_bytes);
43        self.append_data(zone_id, name, data.as_bytes())?;
44
45        // Update IJson metadata counters
46        let file = self.stat(zone_id, name)?.ok_or(StoreError::NotFound)?;
47        let mut meta = file.meta.clone();
48
49        let num_cmds = meta
50            .get(Self::IJSON_NUM_COMMANDS)
51            .and_then(|v| v.as_i64())
52            .unwrap_or(0)
53            + 1;
54        let inc_bytes = meta
55            .get(Self::IJSON_INC_BYTES)
56            .and_then(|v| v.as_i64())
57            .unwrap_or(0)
58            + data.len() as i64;
59
60        meta.insert(
61            Self::IJSON_NUM_COMMANDS.to_string(),
62            serde_json::json!(num_cmds),
63        );
64        meta.insert(
65            Self::IJSON_INC_BYTES.to_string(),
66            serde_json::json!(inc_bytes),
67        );
68        self.write_meta(zone_id, name, meta, false)?;
69
70        // Check compaction thresholds
71        let file_size = file.size.max(1); // avoid division by zero
72        let ratio = inc_bytes as f64 / file_size as f64;
73
74        let should_compact = num_cmds > Self::IJSON_HIGH_COMMANDS
75            || ratio >= Self::IJSON_HIGH_RATIO
76            || (num_cmds > Self::IJSON_LOW_COMMANDS && ratio >= Self::IJSON_LOW_RATIO);
77
78        if should_compact {
79            let _ = self.compact_ijson(zone_id, name);
80        }
81
82        Ok(())
83    }
84
85    /// Compact an IJson file: apply all incremental commands to build compacted state,
86    /// then replace file contents with the compacted result.
87    #[track_caller]
88    pub fn compact_ijson(
89        &self,
90        zone_id: &str,
91        name: &str,
92    ) -> Result<(), StoreError> {
93        // Read full file
94        let data = self
95            .read_file(zone_id, name)?
96            .ok_or(StoreError::NotFound)?;
97
98        let content = String::from_utf8(data)
99            .map_err(|e| StoreError::Other(format!("invalid utf-8 in ijson file: {}", e)))?;
100
101        // Use the ijson module's compact function
102        let compacted = crate::backend::ijson::compact_ijson(&content)
103            .map_err(|e| StoreError::Other(format!("ijson compact error: {}", e)))?;
104
105        // The compacted result is a single JSON command (set at root).
106        // Write it as the new file content.
107        let compacted_with_newline = format!("{}\n", compacted);
108        self.write_file(zone_id, name, compacted_with_newline.as_bytes())?;
109
110        // Reset IJson counters
111        let mut meta = FileMeta::new();
112        meta.insert(
113            Self::IJSON_NUM_COMMANDS.to_string(),
114            serde_json::json!(0),
115        );
116        meta.insert(
117            Self::IJSON_INC_BYTES.to_string(),
118            serde_json::json!(0),
119        );
120        self.write_meta(zone_id, name, meta, true)?;
121
122        Ok(())
123    }
124}