diff --git a/vlib/x/templating/dtm/README.md b/vlib/x/templating/dtm/README.md
index af397e5a316c18..4034ed764ed315 100644
--- a/vlib/x/templating/dtm/README.md
+++ b/vlib/x/templating/dtm/README.md
@@ -3,6 +3,15 @@
A simple template manager integrated into the V project, designed to combine the power of V
templates with Veb, without the need to recompile the application with every change.
+> **Status:** `x.templating.dtm` is the legacy version of Dynamic Template
+> Manager. New projects should use `x.templating.dtm2`, which provides the
+> modern runtime renderer and parsed-template cache.
+>
+> Existing `dtm` users can keep their current API while migrating progressively:
+> the v1 facade now delegates rendering to the DTM2 engine internally, preserving
+> source compatibility while removing the old rendered-cache server from the
+> main rendering path.
+
## Quick Start
Using the dynamic template manager (named `dtm` in this readme) is relatively straightforward.
diff --git a/vlib/x/templating/dtm/dynamic_template_manager.v b/vlib/x/templating/dtm/dynamic_template_manager.v
index 67505fd2f9c66a..4b0016af7c9bcc 100644
--- a/vlib/x/templating/dtm/dynamic_template_manager.v
+++ b/vlib/x/templating/dtm/dynamic_template_manager.v
@@ -1,32 +1,46 @@
+@[deprecated: 'use x.templating.dtm2 for new code']
+@[deprecated_after: '2999-01-01']
module dtm
import os
import crypto.md5
import hash.fnv1a
import time
-import regex
+import strings
+import x.templating.dtm2
// These are all the types of dynamic values that the DTM allows to be returned in the context of a map
+
+@[deprecated: 'use string placeholders with x.templating.dtm2.RenderParams for new code']
+@[deprecated_after: '2999-01-01']
pub type DtmMultiTypeMap = f32 | f64 | i16 | i64 | i8 | int | string | u16 | u32 | u64 | u8
// type MiddlewareFn = fn (mut Context, string) bool
// cache_delay_expiration_at_min is the minimum setting for cache expiration delay, fixed at 5 minutes (measured in seconds).
+
+@[deprecated: 'use x.templating.dtm2 for new code']
+@[deprecated_after: '2999-01-01']
pub const cache_delay_expiration_at_min = 300
// cache_delay_expiration_at_max maximal is the maximal setting for cache expiration delay, fixed at 1 year (measured in seconds).
+
+@[deprecated: 'use x.templating.dtm2 for new code']
+@[deprecated_after: '2999-01-01']
pub const cache_delay_expiration_at_max = 31536000
// cache_delay_expiration_by_default is the default setting for cache expiration delay, fixed at 1 day (measured in seconds).
+
+@[deprecated: 'use x.templating.dtm2 for new code']
+@[deprecated_after: '2999-01-01']
pub const cache_delay_expiration_by_default = 86400
-// Setting channel capacity of the cache handler.
+// Setting channel capacity of the legacy cache handler compatibility channel.
const cache_handler_channel_cap = 200
-// Setting the maximum data size (500 KB) to be stored at memory mode. If this limit is exceeded, cache handler switch to disk mode.
+// Setting the maximum data size (500 KB) to be stored at memory mode. If this limit is exceeded, rendered cache switches to disk mode.
const max_size_data_in_memory = 500
// Defines the maximum character length for placeholder keys.
const max_placeholders_key_size = 50
// Sets the maximum character length for placeholder values.
const max_placeholders_value_size = 3000
-// Internal DTM operations utilize microseconds for time management, primarily aims to minimize the risk of collisions during the creation of temporary cache files
-// especially in cases of simultaneous requests that do not yet have a cache.
+// Internal DTM operations use microseconds to keep cache generation timestamps monotonic.
const convert_seconds = i64(1000000)
const converted_cache_delay_expiration_at_min = i64(cache_delay_expiration_at_min) * convert_seconds
const converted_cache_delay_expiration_at_max = i64(cache_delay_expiration_at_max) * convert_seconds
@@ -38,6 +52,8 @@ const message_signature_info = '[Dynamic Template Manager] Info :'
const message_signature_error = '[Dynamic Template Manager] Error :'
const message_signature_warn = '[Dynamic Template Manager] Warning :'
+@[deprecated: 'use x.templating.dtm2 for new code']
+@[deprecated_after: '2999-01-01']
pub enum CacheStorageMode {
memory
disk
@@ -56,6 +72,8 @@ enum TemplateType {
text
}
+@[deprecated: 'use x.templating.dtm2.Manager for new code']
+@[deprecated_after: '2999-01-01']
@[heap]
pub struct DynamicTemplateManager {
mut:
@@ -67,10 +85,12 @@ mut:
template_folder string
// cache database
template_caches shared []TemplateCache = []TemplateCache{}
+ // Fast lookup from a full template path hash to the active rendered-cache id.
+ template_cache_ids_by_path_hash map[u64]int = map[u64]int{}
// counter for each individual TemplateCache created/updated
id_counter int = 1
ch_cache_handler chan TemplateCache = chan TemplateCache{cap: cache_handler_channel_cap}
- // 'id_to_handlered' field is used exclusively by the cache handler to update or delete specific 'TemplateCache' in the cache database.
+ // 'id_to_handlered' is preserved for legacy callers that route cache update/delete work through the old API.
id_to_handlered int
close_cache_handler bool
// Initialisation params options for these two (Compress_html and active_cache_server)
@@ -88,12 +108,15 @@ mut:
html_file_info shared map[string]HtmlFileInfo = map[string]HtmlFileInfo{}
// Indicates whether the cache file storage directory is located in a temporary OS area
cache_folder_is_temporary_storage bool
- // Handler for all threads used in the DTM
+ // Handler for legacy cache-handler compatibility work.
threads_handler []thread = []thread{}
- // This channel used only for CI. Allows to check during CI tests in case of slowness in the creation/management of the cache to allow enough time for it to be done
+ // This channel is used by legacy tests/callers to observe cache-handler completion.
is_ready chan bool = chan bool{cap: 5}
- // If despite the synchronization attempt during the cache handler tests nothing happens, cancel the tests targeting the cached data
+ // If synchronization times out in legacy cache-handler tests, they set this flag.
abort_test bool
+ // Runtime rendering engine used by the compatibility layer. It caches parsed
+ // template structures only, never rendered HTML responses.
+ render_engine &dtm2.Manager = unsafe { nil }
}
// Represent individual template cache in database memory.
@@ -102,6 +125,8 @@ struct TemplateCache {
mut:
id int
name string
+ // Previous cache id replaced by this cache request, only set for update requests.
+ old_id int
// 'path' field contains the full path, name and file extension of targeted HTML template.
path string
// Checksum of the cached template, which is constructed as follows: the complete HTML content plus its full file path and the timestamp of the cache generation.
@@ -111,18 +136,16 @@ mut:
// and the checksum is then calculated based on this string.
// Given that the value of this checksum is specific to its parent template, there is no issue of collision, and it does not need to be unique.
content_checksum string
- // Name of the temporary template created to hold the content until the cache manager processes and handles the request accordingly.
- // This file is then deleted once it has been utilized.
- tmp_name_file string
// The timestamp of the last modification of the file (HTML template)
last_template_mod i64
// Timestamp of cache generation.
generate_at i64
// Timestamp of cache expiration define by user.
cache_delay_expiration i64
- html_data []u8
- cache_request CacheRequest
- cache_storage_mode CacheStorageMode
+ // Rendered HTML kept directly in memory when the cache entry uses memory storage.
+ html_data string
+ cache_request CacheRequest
+ cache_storage_mode CacheStorageMode
// This field is special as it determines if a cache is obsolete but still in use, allowing the system to redirect to the updated cache.
// This enables the eventual deletion of the obsolete cache without causing issues for requests utilizing the cache.
id_redirection int
@@ -131,8 +154,8 @@ mut:
}
// Represents controls stored in the 'nbr_of_remaining_template_request' of the DTM.
-// It is used to monitor whether a HTML cache template is in use in 'template_caches' and to manage cache deletion procedures
-// as soon as they become feasible and if required of course.
+// It is used to monitor whether a rendered cache entry is in use and to manage
+// deferred deletion when a cache entry is replaced.
@[noinit]
struct RemainingTemplateRequest {
// The id is similar to the one it represents in template_caches.
@@ -140,10 +163,10 @@ struct RemainingTemplateRequest {
mut:
// This value determines the number of ongoing requests using the cache specified by the ID.
nbr_of_remaining_request int
- // The cache has become obsolete, and permission to dispose of it is granted to the cache manager
+ // The cache has become obsolete, and permission to dispose of it is granted to the cache lifecycle.
need_to_delete bool
- // If the cache manager is unable to destroy an obsolete cache (due to it still being actively used),
- // then this boolean allows for the retransmission of the request, enabling action to be taken in the near future.
+ // If an obsolete cache cannot be destroyed because it is still in use, this
+ // boolean allows deletion to be retried when the active request finishes.
need_to_send_delete_request bool
}
@@ -154,7 +177,14 @@ struct HtmlFileInfo {
file_type TemplateType
}
+struct PlaceholderChecksumValue {
+ key string
+ value string
+}
+
// TemplateCacheParams are used to specify cache expiration delay and provide placeholder data for substitution in templates.
+@[deprecated: 'use x.templating.dtm2.RenderParams for new code']
+@[deprecated_after: '2999-01-01']
@[params]
pub struct TemplateCacheParams {
pub:
@@ -163,6 +193,8 @@ pub:
}
// DynamicTemplateManagerInitialisationParams is used with 'initialize' function. (See below at initialize section)
+@[deprecated: 'use x.templating.dtm2.ManagerParams for new code']
+@[deprecated_after: '2999-01-01']
@[params]
pub struct DynamicTemplateManagerInitialisationParams {
pub:
@@ -186,11 +218,13 @@ pub:
// - active_cache_server: 'type bool' Activate or not the template cache system. ( default is true )
// - test_cache_dir: 'type string' Used by DTM internal tests to override the cache directory.
// - test_template_dir: 'type string' Used by DTM internal tests to override the templates directory.
+@[deprecated: 'use x.templating.dtm2.initialize for new code']
+@[deprecated_after: '2999-01-01']
pub fn initialize(dtm_init_params DynamicTemplateManagerInitialisationParams) &DynamicTemplateManager {
mut dir_path := dtm_init_params.def_cache_path
mut dir_html_path := os.join_path('${os.dir(os.executable())}/templates')
mut max_size_memory := 0
- mut active_cache_handler := dtm_init_params.active_cache_server
+ mut active_rendered_cache := dtm_init_params.active_cache_server
mut system_ready := true
mut cache_temporary_bool := false
@@ -200,7 +234,7 @@ pub fn initialize(dtm_init_params DynamicTemplateManagerInitialisationParams) &D
if dtm_init_params.test_template_dir != '' {
dir_html_path = dtm_init_params.test_template_dir
}
- if active_cache_handler {
+ if active_rendered_cache {
// Control if cache folder created by user exist
if dir_path != '' && os.exists(dir_path) && os.is_dir(dir_path) {
// WARNING: When setting the directory for caching files and for testing purposes,
@@ -215,18 +249,18 @@ pub fn initialize(dtm_init_params DynamicTemplateManagerInitialisationParams) &D
dir_path = os.join_path(os.temp_dir(), 'vcache_dtm')
if !os.exists(dir_path) || !os.is_dir(dir_path) {
os.mkdir(dir_path) or {
- active_cache_handler = false
+ active_rendered_cache = false
eprintln(err.msg())
}
}
- if active_cache_handler {
+ if active_rendered_cache {
check_and_clear_cache_files(dir_path) or {
- active_cache_handler = false
+ active_rendered_cache = false
eprintln(err.msg())
}
}
// If it is impossible to use a cache directory, the cache system is deactivated, and the user is warned."
- if !active_cache_handler {
+ if !active_rendered_cache {
eprintln('${message_signature_warn} The cache storage directory does not exist or has a problem and it was also not possible to use a folder suitable for temporary storage. Therefore, the cache system will be disabled. It is recommended to address the aforementioned issues to utilize the cache system.')
} else {
cache_temporary_bool = true
@@ -252,21 +286,24 @@ pub fn initialize(dtm_init_params DynamicTemplateManagerInitialisationParams) &D
}
mut dtmi := &DynamicTemplateManager{
- template_cache_folder: dir_path
- template_folder: dir_html_path
+ template_cache_folder: dir_path.clone()
+ template_folder: dir_html_path.clone()
+ template_cache_ids_by_path_hash: map[u64]int{}
max_size_data_in_memory: max_size_memory
compress_html: dtm_init_params.compress_html
- active_cache_server: active_cache_handler
+ active_cache_server: active_rendered_cache
c_time: get_current_unix_micro_timestamp()
dtm_init_is_ok: system_ready
cache_folder_is_temporary_storage: cache_temporary_bool
+ render_engine: new_dtm2_render_engine(dir_html_path,
+ dtm_init_params.compress_html)
}
if system_ready {
- // Disable cache handler if user doesn't required. Else, new thread is used to start the cache system. ( By default is ON )
- if active_cache_handler {
+ if active_rendered_cache {
dtmi.threads_handler << spawn dtmi.cache_handler()
- dtmi.threads_handler << spawn dtmi.handle_dtm_clock()
}
+ // Rendered-cache work is processed synchronously. This keeps prod/Boehm behavior
+ // deterministic while preserving DTM's dynamic rendered-cache feature.
println('${message_signature} Dynamic Template Manager activated')
} else {
eprintln('${message_signature_error} Unable to use the Dynamic Template Manager, please refer to the above errors and correct them.')
@@ -294,77 +331,45 @@ fn init_cache_block_middleware(cache_dir string, mut dtm &DynamicTemplateManager
// To use this function, HTML templates must be located in the 'templates' directory at the project's root.
// However, it allows the use of subfolder paths within the 'templates' directory,
// enabling users to structure their templates in a way that best suits their project's organization.
+@[deprecated: 'use x.templating.dtm2.Manager.expand for new code']
+@[deprecated_after: '2999-01-01']
pub fn (mut tm DynamicTemplateManager) expand(tmpl_path string, tmpl_var TemplateCacheParams) string {
- if tm.dtm_init_is_ok {
- file_path, tmpl_name, current_content_checksum, tmpl_type := tm.check_tmpl_and_placeholders_size(tmpl_path,
- tmpl_var.placeholders) or { return err.msg() }
- converted_cache_delay_expiration := i64(tmpl_var.cache_delay_expiration) * convert_seconds
- // If cache exist, return necessary fields else, 'is_cache_exist' return false.
- is_cache_exist, id, path, mut last_template_mod, gen_at, cache_del_exp, content_checksum :=
- tm.return_cache_info_isexistent(file_path)
- mut html := ''
- // Definition of several variables used to assess the need for cache updates.sss
- // This determination is based on modifications within the HTML template itself.
- // `last_template_mod` is set to 0 and `test_current_template_mod` to `i64(0)` when a new cache needs to be created.
- // `test_current_template_mod` is utilized for updating the cache.
- mut test_current_template_mod := i64(0)
- if last_template_mod == 0 {
- // Get last modification timestamp of HTML template to adding info for the creation of cache.
- last_template_mod = os.file_last_mod_unix(file_path)
- } else {
- // Get last modification timestamp of HTML template to compare with cache info already existent.
- test_current_template_mod = os.file_last_mod_unix(file_path)
- }
-
- // From this point, all the previously encountered variables are used to determine the routing in rendering the HTML template and creating/using its cache.
- cash_req, unique_time := tm.cache_request_route(is_cache_exist,
- converted_cache_delay_expiration, last_template_mod, test_current_template_mod,
- cache_del_exp, gen_at, tm.c_time, content_checksum, current_content_checksum)
- // Each of these match statements aims to provide HTML rendering, but each one sends a specific signal 'cash_req' of type CacheRequest
- // or calls the appropriate function for managing the cache of the provided HTML.
- match cash_req {
- .new {
- // Create a new cache
- html = tm.create_template_cache_and_display(cash_req, last_template_mod,
- unique_time, file_path, tmpl_name, converted_cache_delay_expiration,
- tmpl_var.placeholders, current_content_checksum, tmpl_type)
- // println('create cache : ${cash_req}')
- }
- .update, .exp_update {
- // Update an existing cache
- tm.id_to_handlered = id
- html = tm.create_template_cache_and_display(cash_req, test_current_template_mod,
- unique_time, file_path, tmpl_name, converted_cache_delay_expiration,
- tmpl_var.placeholders, current_content_checksum, tmpl_type)
- // println('update cache : ${cash_req}')
- }
- else {
- // Use the provided cache of html template.
- html = tm.get_cache(tmpl_name, path, tmpl_var.placeholders)
- // println('get cache : ${cash_req}')
- }
- }
-
- return html
- } else {
- tm.stop_cache_handler()
+ if !tm.dtm_init_is_ok {
+ tm.disable_cache()
eprintln('${message_signature_error} The initialization phase of DTM has failed. Therefore, you cannot use it. Please address the errors and then restart the dtm server.')
return internat_server_error
}
+ return tm.expand_with_dtm2_render_engine(tmpl_path, tmpl_var)
}
-// stop_cache_handler signals the termination of the cache handler.
-// It does so by setting 'close_cache_handler' to true, and sending a signal
-// through the channel which will trigger a cascading effect to close the cache
-// handler thread as well as the DTM clock thread.
+// disable_cache disables future rendered-cache storage for this manager.
+@[deprecated: 'legacy DTM compatibility only; use x.templating.dtm2 for new code']
+@[deprecated_after: '2999-01-01']
+pub fn (mut tm DynamicTemplateManager) disable_cache() {
+ tm.stop_legacy_cache_handler()
+}
+
+// stop_cache_handler is kept for backward compatibility with the former cache-server
+// API. Rendered-cache work is now synchronous, so stopping it simply disables future
+// rendered-cache storage for this manager.
+@[deprecated: 'use disable_cache while maintaining legacy DTM code; use x.templating.dtm2 for new code']
+@[deprecated_after: '2999-01-01']
pub fn (mut tm DynamicTemplateManager) stop_cache_handler() {
- if tm.active_cache_server {
- tm.active_cache_server = false
- tm.close_cache_handler = true
- tm.ch_cache_handler <- TemplateCache{
- id: 0
- }
+ tm.stop_legacy_cache_handler()
+}
+
+fn (mut tm DynamicTemplateManager) stop_legacy_cache_handler() {
+ if !tm.active_cache_server {
+ return
+ }
+ tm.active_cache_server = false
+ tm.close_cache_handler = true
+ tm.ch_cache_handler <- TemplateCache{
+ id: 0
+ }
+ if tm.threads_handler.len > 0 {
tm.threads_handler.wait()
+ tm.threads_handler = []thread{}
}
}
@@ -426,8 +431,9 @@ fn (mut tm DynamicTemplateManager) check_tmpl_and_placeholders_size(f_path strin
rlock tm.html_file_info {
if mapped_html_info := tm.html_file_info[f_path] {
- html_file = mapped_html_info.file_full_path
- file_name = mapped_html_info.file_name
+ html_file = mapped_html_info.file_full_path.clone()
+ file_name = mapped_html_info.file_name.clone()
+ define_file_type = mapped_html_info.file_type
} else {
need_to_create_entry = true
}
@@ -453,9 +459,9 @@ fn (mut tm DynamicTemplateManager) check_tmpl_and_placeholders_size(f_path strin
define_file_type = TemplateType.text
}
lock tm.html_file_info {
- tm.html_file_info[f_path] = HtmlFileInfo{
- file_full_path: html_file
- file_name: file_name
+ tm.html_file_info[f_path.clone()] = HtmlFileInfo{
+ file_full_path: html_file.clone()
+ file_name: file_name.clone()
file_type: define_file_type
}
}
@@ -469,34 +475,38 @@ fn (mut tm DynamicTemplateManager) check_tmpl_and_placeholders_size(f_path strin
// If it has, it creates a checksum of this content for analysis.
// The checksum is generated by concatenating all dynamic values and applying a fnv1a hash to the resulting string and generate a checksum.
if tmpl_var.len > 0 {
- mut combined_str := ''
- // Control placeholder key and value sizes
+ mut placeholder_values := []PlaceholderChecksumValue{cap: tmpl_var.len}
for key, value in tmpl_var {
if key.len > max_placeholders_key_size {
eprintln('${message_signature_error} Length of placeholder key "${key}" exceeds the maximum allowed size for template content in file: ${html_file}. Max allowed size: ${max_placeholders_key_size} characters.')
return error(internat_server_error)
}
+ mut casted_value := ''
match value {
string {
- if value.len > max_placeholders_value_size {
- eprintln('${message_signature_error} Length of placeholder value for key "${key}" exceeds the maximum allowed size for template content in file: ${html_file}. Max allowed size: ${max_placeholders_value_size} characters.')
- return error(internat_server_error)
- }
- combined_str += value
+ casted_value = value
}
else {
- casted_value := value.str()
- if casted_value.len > max_placeholders_value_size {
- eprintln('${message_signature_error} Length of placeholder value for key "${key}" exceeds the maximum allowed size for template content in file: ${html_file}. Max allowed size: ${max_placeholders_value_size} characters.')
- return error(internat_server_error)
- }
-
- combined_str += casted_value
+ casted_value = value.str()
}
}
+
+ if casted_value.len > max_placeholders_value_size {
+ eprintln('${message_signature_error} Length of placeholder value for key "${key}" exceeds the maximum allowed size for template content in file: ${html_file}. Max allowed size: ${max_placeholders_value_size} characters.')
+ return error(internat_server_error)
+ }
+ placeholder_values << PlaceholderChecksumValue{
+ key: key.clone()
+ value: casted_value.clone()
+ }
+ }
+ placeholder_values.sort(a.key < b.key)
+ mut combined_str := strings.new_builder(placeholder_values.len * 16)
+ for placeholder_value in placeholder_values {
+ combined_str.write_string(placeholder_value.value)
}
- res_checksum_content = fnv1a.sum64_string(combined_str).str()
+ res_checksum_content = fnv1a.sum64_string(combined_str.str()).str()
}
// If all is ok, return full path of template file and filename without extension
@@ -504,14 +514,13 @@ fn (mut tm DynamicTemplateManager) check_tmpl_and_placeholders_size(f_path strin
}
// create_template_cache_and_display is exclusively invoked from `expand`.
-// It generates the template rendering and relaying information
-// to the cache manager for either the creation or updating of the template cache.
-// It begin to starts by ensuring that the cache delay expiration is correctly set by user.
+// It generates the template rendering and prepares rendered-cache information
+// for either the creation or updating of the template cache.
+// It starts by ensuring that the cache delay expiration is correctly set by user.
// It then parses the template file, replacing placeholders with actual dynamics/statics values.
// If caching is enabled (indicated by a cache delay expiration different from -1) and the template content is valid,
// the function constructs a `TemplateCache` request with all the necessary details.
-// This request is then sent to the cache handler channel, signaling either the need for a new cache or an update to an existing one.
-// The function returns the rendered immediately, without waiting for the cache to be created or updated.
+// This request is processed synchronously, so the rendered cache is ready when the function returns.
fn (mut tm DynamicTemplateManager) create_template_cache_and_display(tcs CacheRequest, last_template_mod i64,
unique_time i64, file_path string, tmpl_name string, cache_delay_expiration i64, placeholders &map[string]DtmMultiTypeMap,
current_content_checksum string, tmpl_type TemplateType) string {
@@ -522,31 +531,36 @@ fn (mut tm DynamicTemplateManager) create_template_cache_and_display(tcs CacheRe
}
// Parses the template and stores the rendered output in the variable. See the function itself for more details.
mut html := tm.parse_tmpl_file(file_path, tmpl_name, placeholders, tm.compress_html, tmpl_type)
- // If caching is enabled and the template content is valid, this section creates a temporary cache file, which is then used by the cache manager.
- // If successfully temporary is created, a cache creation/update notification is sent through its dedicated channel to the cache manager
+ // If caching is enabled and the template content is valid, this section creates or updates
+ // the rendered cache synchronously. Keeping the rendered string in the current call avoids
+ // fragile tmp-file ownership/conversion paths in prod builds.
if cache_delay_expiration != -1 && html != internat_server_error && tm.active_cache_server {
- op_success, tmp_name := tm.create_temp_cache(html, file_path, unique_time)
- if op_success {
- tm.ch_cache_handler <- TemplateCache{
- id: tm.id_counter
- name: tmpl_name
- // 'path' field contains the full path, name and file extension of targeted HTML template.
- path: file_path
- content_checksum: current_content_checksum
- tmp_name_file: tmp_name
- // Last modified timestamp of HTML template
- last_template_mod: last_template_mod
- // Unix current local timestamp of cache generation request converted to UTC
- generate_at: unique_time
- // Defines the cache expiration delay in seconds. This value is added to 'generate_at' to calculate the expiration time of the cache.
- cache_delay_expiration: cache_delay_expiration
- // The requested routing to define creation or updating cache.
- cache_request: tcs
- }
- // In the context of a cache update, this function is used to signal that the process has finished using the cache information. The 'nbr_of_remaining_request' counter is therefore updated."
- if tcs == .update || tcs == .exp_update {
- tm.remaining_template_request(false, tm.id_to_handlered)
- }
+ cache_id := tm.id_counter
+ tm.id_counter++
+ old_cache_id := tm.id_to_handlered
+ cache_request := TemplateCache{
+ id: cache_id
+ name: tmpl_name.clone()
+ // 'path' field contains the full path, name and file extension of targeted HTML template.
+ path: file_path.clone()
+ content_checksum: current_content_checksum.clone()
+ // Last modified timestamp of HTML template
+ last_template_mod: last_template_mod
+ // Unix current local timestamp of cache generation request converted to UTC
+ generate_at: unique_time
+ // Defines the cache expiration delay in seconds. This value is added to 'generate_at' to calculate the expiration time of the cache.
+ cache_delay_expiration: cache_delay_expiration
+ html_data: html.clone()
+ // The requested routing to define creation or updating cache.
+ cache_request: tcs
+ old_id: old_cache_id
+ }
+ mut process_request := cache_request
+ tm.process_cache_request(mut process_request)
+ tm.signal_cache_ready()
+ // In the context of a cache update, this function is used to signal that the process has finished using the cache information. The 'nbr_of_remaining_request' counter is therefore updated."
+ if tcs == .update || tcs == .exp_update {
+ tm.remaining_template_request(false, old_cache_id)
}
} else {
// In the context of a template validity error, the 'nbr_of_remaining_request' counter is consistently updated to avoid anomalies in cache management.
@@ -556,123 +570,151 @@ fn (mut tm DynamicTemplateManager) create_template_cache_and_display(tcs CacheRe
return html
}
-// create_temp_cache is responsible for creating a temporary cache file, which is subsequently used exclusively by the cache manager.
-// It generates a temporary file name using a checksum based on the timestamp and the file path.
-// The content is then written to this file, located in the designated cache folder.
-// If the operation is successful, a boolean is returned to allow for the sending of a create or modify request to the cache manager.
-fn (tm DynamicTemplateManager) create_temp_cache(html &string, f_path string, ts i64) (bool, string) {
- // Extracts the base file name with extension from the given file path.
- file_name_with_ext := os.base(f_path)
- // Removes the file extension, keeping only the name.
- file_name := file_name_with_ext.all_before_last('.')
- // Combines the timestamp and file path into a single string
- combined_str := ts.str() + f_path
- // Generates a md5 hash of the combined string for uniqueness
- tmp_checksum := md5.hexhash(combined_str)
- // Forms the temporary file name using the file name, checksum, and a .tmp extension
- tmp_name := '${file_name}_${tmp_checksum}.tmp'
- // Creates the full path for the temporary file in the cache folder
- cache_path := os.join_path(tm.template_cache_folder, tmp_name)
- // Converts the HTML content into a byte array
- html_bytes := html.bytes()
- mut f := os.create(cache_path) or {
- eprintln('${message_signature_error} Cannot create temporary cache file : ${err.msg()}')
- return false, ''
+// get_cache is exclusively invoked from `expand', retrieves the rendered HTML from the cache.
+fn (mut tm DynamicTemplateManager) get_cache(_name string, path string, _placeholders &map[string]DtmMultiTypeMap) string {
+ mut cache_id := 0
+ rlock tm.template_caches {
+ for value in tm.template_caches {
+ // If the cache for the specified HTML template is found, perform the following operations:
+ if value.path == path {
+ cache_id = value.id
+ break
+ }
+ }
}
- f.write(html_bytes) or {
- eprintln('${message_signature_error} Cannot write in temporary cache file : ${err.msg()}')
- f.close()
- return false, ''
+ if cache_id != 0 {
+ return tm.get_cache_by_id(cache_id)
}
- f.close()
- return true, tmp_name
+ return ''
}
-// get_cache is exclusively invoked from `expand', retrieves the rendered HTML from the cache.
-fn (mut tm DynamicTemplateManager) get_cache(name string, path string, placeholders &map[string]DtmMultiTypeMap) string {
- mut html := ''
- // Lock the cache database for writing.
+fn (mut tm DynamicTemplateManager) get_cache_by_id(cache_id int) string {
+ mut cache_exists := false
+ mut html_data := ''
+ mut cache_storage_mode := CacheStorageMode.memory
+ mut disk_cache_path := ''
+ mut cache_name := ''
rlock tm.template_caches {
for value in tm.template_caches {
- // If the cache for the specified HTML template is found, perform the following operations:
- if value.path == path {
- match value.cache_storage_mode {
- .memory {
- // Retrieve the HTML render from the memory cache and convert it to a string.
- html = value.html_data.bytestr()
- }
- .disk {
- r_b_html := os.read_bytes(value.cache_full_path_name) or {
- eprintln('${message_signature_error} Get_cache() cannot read template cache file ${value.name} : ${err.msg()} ')
- return internat_server_error
- }
- html = r_b_html.bytestr()
- }
+ if value.id == cache_id {
+ cache_exists = true
+ html_data = value.html_data.clone()
+ cache_storage_mode = value.cache_storage_mode
+ cache_name = value.name.clone()
+ disk_cache_path = if value.cache_full_path_name != '' {
+ value.cache_full_path_name.clone()
+ } else {
+ tm.cache_disk_path(value.name, value.checksum)
}
-
- // Function is used to signal that the process has finished using the cache information. The 'nbr_of_remaining_request' counter is therefore updated."
- tm.remaining_template_request(false, value.id)
- return html
+ break
}
}
}
+ defer {
+ if cache_exists {
+ tm.remaining_template_request(false, cache_id)
+ }
+ }
+ if html_data.len > 0 {
+ return html_data
+ }
+ match cache_storage_mode {
+ .memory {
+ return ''
+ }
+ .disk {
+ cached_html := os.read_file(disk_cache_path) or {
+ eprintln('${message_signature_error} Get_cache() cannot read template cache file ${cache_name} : ${err.msg()} ')
+ return internat_server_error
+ }
+ return cached_html
+ }
+ }
- return html
+ return ''
}
// return_cache_info_isexistent is exclusively used in 'expand' to determine whether a cache exists for the provided HTML template.
// If a cache exists, it returns the necessary information for its transformation. If not, it indicates the need to create a new cache.
fn (mut tm DynamicTemplateManager) return_cache_info_isexistent(tmpl_path string) (bool, int, string, i64, i64, i64, string) {
- // Lock the cache database for writing.
- rlock tm.template_caches {
- for value in tm.template_caches {
- if value.path == tmpl_path {
- // This code section handles cache redirection.
- // If a cache redirection ID is found, it indicates that the currently used cache is outdated and there's a newer version available.
- // The process then seeks to retrieve information from this more recent cache.
- // This is done recursively: if the updated cache itself points to an even newer version, the process continues until the most up-to-date cache is found.
- // This recursive mechanism ensures that the latest cache data is always used.
- if value.id_redirection != 0 {
- mut need_goto := false
- mut id_value_recursion := value.id_redirection
- unsafe {
- re_loop:
- inner_loop: for val in tm.template_caches {
- if val.id == id_value_recursion {
- if val.id_redirection != 0 {
- id_value_recursion = val.id_redirection
- need_goto = true
- break inner_loop
- } else {
- // function is used to signal that the process has begun using the cache information.
- tm.remaining_template_request(true, val.id)
- return true, val.id, val.path, val.last_template_mod, val.generate_at, val.cache_delay_expiration, val.content_checksum
- }
- }
- }
- if need_goto {
- need_goto = false
- goto re_loop
- }
+ cache_entries := tm.snapshot_template_caches()
+ mut cache_found := false
+ mut cache_id := 0
+ mut cache_path := ''
+ mut last_template_mod := i64(0)
+ mut generate_at := i64(0)
+ mut cache_delay_expiration := i64(0)
+ mut content_checksum := ''
+ path_hash := fnv1a.sum64_string(tmpl_path)
+ active_cache_id := tm.template_cache_ids_by_path_hash[path_hash] or { 0 }
+ for value in cache_entries {
+ if active_cache_id != 0 {
+ if value.id != active_cache_id {
+ continue
+ }
+ } else if value.path != tmpl_path {
+ continue
+ }
+ // This code section handles cache redirection.
+ // If a cache redirection ID is found, it indicates that the currently used cache is outdated and there's a newer version available.
+ // The process then seeks to retrieve information from this more recent cache.
+ // This is done recursively: if the updated cache itself points to an even newer version, the process continues until the most up-to-date cache is found.
+ // This recursive mechanism ensures that the latest cache data is always used.
+ cache_found = true
+ cache_id = value.id
+ cache_path = value.path.clone()
+ last_template_mod = value.last_template_mod
+ generate_at = value.generate_at
+ cache_delay_expiration = value.cache_delay_expiration
+ content_checksum = value.content_checksum.clone()
+ if value.id_redirection != 0 {
+ mut id_value_recursion := value.id_redirection
+ for {
+ mut found_redirect := false
+ for val in cache_entries {
+ if val.id == id_value_recursion {
+ cache_id = val.id
+ cache_path = val.path.clone()
+ last_template_mod = val.last_template_mod
+ generate_at = val.generate_at
+ cache_delay_expiration = val.cache_delay_expiration
+ content_checksum = val.content_checksum.clone()
+ found_redirect = true
+ break
}
- // No cache redirection, get cache current information.
- } else {
- // function is used to signal that the process has begun using the cache information.
- tm.remaining_template_request(true, value.id)
- return true, value.id, value.path, value.last_template_mod, value.generate_at, value.cache_delay_expiration, value.content_checksum
}
+ if !found_redirect {
+ break
+ }
+ mut next_redirection := 0
+ for val in cache_entries {
+ if val.id == cache_id {
+ next_redirection = val.id_redirection
+ break
+ }
+ }
+ if next_redirection == 0 {
+ break
+ }
+ id_value_recursion = next_redirection
}
}
+ break
+ }
+ if cache_found {
+ // Function is used to signal that the process has begun using the cache information.
+ tm.remaining_template_request(true, cache_id)
+ return true, cache_id, cache_path, last_template_mod, generate_at, cache_delay_expiration, content_checksum
}
// No existing cache, need to create it.
return false, 0, '', 0, 0, 0, ''
}
-// remaining_template_request manages the counter in 'nbr_of_remaining_template_request', which tracks the number of requests that have started or finished for a specific cache.
-// Moreover, this function sends a cache deletion callback request when the cache manager had previously been instructed to delete the cache but was unable to do because,
-// it was still in use.
+// remaining_template_request manages the counter in 'nbr_of_remaining_template_request',
+// which tracks the number of requests that have started or finished for a specific
+// rendered-cache entry. If a replaced cache is no longer in use, it is deleted.
fn (mut tm DynamicTemplateManager) remaining_template_request(b bool, v int) {
- // Lock the remaining template request process for reading and writing.
+ mut delete_request_id := 0
lock tm.nbr_of_remaining_template_request {
for key, r_request in tm.nbr_of_remaining_template_request {
if r_request.id == v {
@@ -683,13 +725,9 @@ fn (mut tm DynamicTemplateManager) remaining_template_request(b bool, v int) {
// if false, Decrements the count of active cache requests.
tm.nbr_of_remaining_template_request[key].nbr_of_remaining_request -= 1
// Checks if the number of active requests is zero or less and if there's a pending delete request for the cache.
- // If yes, request is sent to the cache handler on this own channel.
- if r_request.nbr_of_remaining_request <= 0
- && r_request.need_to_send_delete_request {
- tm.ch_cache_handler <- TemplateCache{
- id: r_request.id
- cache_request: .delete
- }
+ if tm.nbr_of_remaining_template_request[key].nbr_of_remaining_request <= 0
+ && tm.nbr_of_remaining_template_request[key].need_to_send_delete_request {
+ delete_request_id = r_request.id
}
}
@@ -697,128 +735,156 @@ fn (mut tm DynamicTemplateManager) remaining_template_request(b bool, v int) {
}
}
}
+ if delete_request_id != 0 {
+ mut delete_request := TemplateCache{
+ id: delete_request_id
+ cache_request: .delete
+ }
+ tm.process_cache_request(mut delete_request)
+ }
+}
+
+// process_cache_request stores or deletes one rendered-cache entry synchronously.
+// Small rendered outputs are kept in memory; larger outputs are written to disk.
+fn (mut tm DynamicTemplateManager) process_cache_request(mut tc TemplateCache) bool {
+ tc.ensure_owned_data()
+
+ // Determine if the request is a duplicate. If so, the cache creation/update request is ignored.
+ is_duplicate_request := tm.chandler_prevent_cache_duplicate_request(tc)
+ if is_duplicate_request {
+ return true
+ }
+
+ if tc.cache_request != .delete {
+ rendered_html := tc.html_data.clone()
+ if tc.html_data.len <= (tm.max_size_data_in_memory * 1024) {
+ tc.cache_storage_mode = .memory
+ } else {
+ tc.cache_storage_mode = .disk
+ }
+
+ combined_str := rendered_html + tc.path + tc.generate_at.str()
+ tc.checksum = md5.hexhash(combined_str).clone()
+
+ match tc.cache_storage_mode {
+ .memory {
+ tc.html_data = rendered_html.clone()
+ }
+ .disk {
+ cache_file_path := tm.cache_disk_path(tc.name, tc.checksum)
+ os.write_file(cache_file_path, rendered_html) or {
+ eprintln('${message_signature_error} Rendered cache: failed to write cache file: ${err.msg()}')
+ return false
+ }
+ tc.cache_full_path_name = cache_file_path.clone()
+ // Keep a hot in-memory copy even for disk-backed entries. The disk file
+ // preserves compatibility, while cache hits avoid fragile string metadata
+ // roundtrips in optimized prod builds.
+ tc.html_data = rendered_html.clone()
+ }
+ }
+ }
+
+ if tc.cache_request != .delete {
+ stable_cache_entry := tc.clone_cache_entry()
+ lock tm.template_caches {
+ tm.template_caches << stable_cache_entry
+ }
+ path_hash := fnv1a.sum64_string(stable_cache_entry.path)
+ tm.template_cache_ids_by_path_hash[path_hash] = stable_cache_entry.id
+ }
+ if tc.cache_request == .new {
+ tm.chandler_remaining_cache_template_used(tc.cache_request, tc.id, tc.old_id)
+ } else {
+ test_b := tm.chandler_remaining_cache_template_used(tc.cache_request, tc.id, tc.old_id)
+ if test_b {
+ clear_cache_id := if tc.cache_request == .delete { tc.id } else { tc.old_id }
+ key, is_success := tm.chandler_clear_specific_cache(clear_cache_id)
+ if !is_success {
+ return false
+ }
+ lock tm.template_caches {
+ tm.template_caches.delete(key)
+ }
+ }
+ }
+ return true
+}
+
+fn (tm &DynamicTemplateManager) cache_disk_path(name string, checksum string) string {
+ cache_file_name := '${name}_${checksum}.cache'
+ return os.join_path(tm.template_cache_folder, cache_file_name).clone()
+}
+
+fn (mut tc TemplateCache) ensure_owned_strings() {
+ tc.name = tc.name.clone()
+ tc.path = tc.path.clone()
+ tc.checksum = tc.checksum.clone()
+ tc.content_checksum = tc.content_checksum.clone()
+ tc.cache_full_path_name = tc.cache_full_path_name.clone()
+}
+
+fn (mut tc TemplateCache) ensure_owned_data() {
+ tc.ensure_owned_strings()
+ tc.html_data = tc.html_data.clone()
+}
+
+fn (tc TemplateCache) clone_cache_entry() TemplateCache {
+ return TemplateCache{
+ id: tc.id
+ name: tc.name.clone()
+ old_id: tc.old_id
+ path: tc.path.clone()
+ checksum: tc.checksum.clone()
+ content_checksum: tc.content_checksum.clone()
+ last_template_mod: tc.last_template_mod
+ generate_at: tc.generate_at
+ cache_delay_expiration: tc.cache_delay_expiration
+ html_data: tc.html_data.clone()
+ cache_request: tc.cache_request
+ cache_storage_mode: tc.cache_storage_mode
+ id_redirection: tc.id_redirection
+ cache_full_path_name: tc.cache_full_path_name.clone()
+ }
+}
+
+fn (tm &DynamicTemplateManager) snapshot_template_caches() []TemplateCache {
+ rlock tm.template_caches {
+ mut entries := []TemplateCache{cap: tm.template_caches.len}
+ for entry in tm.template_caches {
+ entries << entry.clone_cache_entry()
+ }
+ return entries
+ }
+}
+
+fn (mut tm DynamicTemplateManager) signal_cache_ready() {
+ $if test {
+ select {
+ tm.is_ready <- true {}
+ else {}
+ }
+ }
}
-// fn (mut DynamicTemplateManager) cache_handler()
-//
-// This function serves as the core handler for managing the cache within the DTM.
-// It continuously listens for cache update requests and processes them accordingly.
-// When a cache request is received, it either creates or updates or deletes the cache based on the request type.
-// The function ensures that duplicate cache requests are avoided and handles the closing of the cache handler.
-// It is necessary to restart the entire application in case the manager closes.
-// The manager handles cache operations in a multithreaded context, accepting up to a list of 200 operation requests. (Define in 'cache_handler_channel_cap' constant)
-// The HTML rendering is stored as a u8 array.
-//
-// TODO: Currently, the cache manager stops when it encounters an internal error requiring a restart of the program.
-// ( it is designed to ignore external errors since these are already handled in a way that ensures no cache processing requests are affected ),
-// A recovery system will need to be implemented to ensure service continuity.
-//
fn (mut tm DynamicTemplateManager) cache_handler() {
defer {
- // If cause is an internal cache handler error
- tm.active_cache_server = false
- // Close channel if handler is stopped
tm.ch_cache_handler.close()
tm.is_ready.close()
- tm.ch_stop_dtm_clock <- true
}
for {
select {
- // Continuously listens until a request is received through the dedicated channel.
- mut tc := <-tm.ch_cache_handler {
- // Close handler if asked.
+ tc := <-tm.ch_cache_handler {
if tm.close_cache_handler {
eprintln('${message_signature_info} Cache manager has been successfully stopped. Please consider restarting the application if needed.')
- break
+ return
}
- f_path_tmp := os.join_path(tm.template_cache_folder, tc.tmp_name_file)
-
- // determine if the requests passed to the manager are not duplicate requests. If so, the temporary file will be destroyed as part of a cache creation/update request.
- if !tm.chandler_prevent_cache_duplicate_request(tc) {
- if tc.cache_request != .delete {
- // Determines the size of the template content to decide where to store it (in memory or on disk).
- // It retrieves the content from the temporary file and then forms a unique checksum for the final name of the cache file.
- // The file size is compared to the maximum allowable data size in memory.
- // If the size is within the limit, the cache is stored in memory; otherwise, it's stored on disk.
- // The unique checksum is generated by hashing the file data, its path, and the cache generation timestamp using md5.
- tmp_file_size := os.file_size(f_path_tmp)
- if tmp_file_size <= (tm.max_size_data_in_memory * 1024) {
- tc.cache_storage_mode = .memory
- } else {
- tc.cache_storage_mode = .disk
- }
- file_data := os.read_bytes(f_path_tmp) or {
- eprintln('${message_signature_error} Cache Handler : Failed to read tmp file, cache server will be stopped, you need to fix and restart application: ${err.msg()}')
- break
- }
-
- combined_str := file_data.str() + tc.path + tc.generate_at.str()
- tc.checksum = md5.hexhash(combined_str)
-
- match tc.cache_storage_mode {
- .memory {
- // If the cache is stored in memory, the temporary file is destroyed.
- tc.html_data = file_data
- os.rm(f_path_tmp) or {
- eprintln('${message_signature_error} Cache Handler : While deleting the tmp cache file: "${f_path_tmp}", cache server will be stopped, you need to fix and restart application: ${err.msg()}')
- break
- }
- }
- .disk {
- // If the cache is stored on disk, the temporary file is renamed to become the definitive cache of the current version of the HTML template.
- tc.cache_full_path_name = os.join_path(tm.template_cache_folder,
- '${tc.name}_${tc.checksum}.cache')
- os.mv(f_path_tmp, tc.cache_full_path_name) or {
- eprintln('${message_signature_error} Cache Handler : Failed to rename tmp file, cache server will be stopped, you need to fix and restart application: ${err.msg()}')
- break
- }
- }
- }
- }
- // Lock the cache database for reading and writing.
- lock tm.template_caches {
- if tc.cache_request != .delete {
- // Include Cache information in database.
- tm.template_caches << tc
-
- $if test {
- tm.is_ready <- true
- }
- }
- if tc.cache_request == .new {
- tm.chandler_remaining_cache_template_used(tc.cache_request, tc.id,
- tm.id_to_handlered)
- // Increment ID counter for the next creation/update cache request
- tm.id_counter++
- } else {
- if tc.cache_request != .delete {
- tm.id_counter++
- }
- // This function allows the cache manager to handle what happens in 'nbr_of_remaining_template_request' and
- // act accordingly for the creation, update, or destruction of the cache.
- test_b := tm.chandler_remaining_cache_template_used(tc.cache_request,
- tc.id, tm.id_to_handlered)
- if test_b {
- // Finding position of cache in database ( If disk mode, cache is erased )
- key, is_success :=
- tm.chandler_clear_specific_cache(tm.id_to_handlered)
- if !is_success {
- break
- }
- // Delete in database.
- tm.template_caches.delete(key)
-
- $if test {
- tm.is_ready <- true
- }
- }
- }
- }
- } else if tc.cache_request != .delete {
- os.rm(f_path_tmp) or {
- eprintln('${message_signature_warn} Cache Handler : Cannot deleting the unused tmp cache file: "${f_path_tmp}" : ${err.msg()}')
- }
+ mut request := tc
+ if request.cache_request != .delete && request.old_id == 0 {
+ request.old_id = tm.id_to_handlered
}
+ tm.process_cache_request(mut request)
+ tm.signal_cache_ready()
}
}
}
@@ -826,11 +892,11 @@ fn (mut tm DynamicTemplateManager) cache_handler() {
// fn (DynamicTemplateManager) chandler_prevent_cache_duplicate_request(&TemplateCache) return bool
//
-// Exclusively used by the cache handler, assesses whether a cache request is a duplicate,
+// Used by rendered-cache processing to assess whether a cache request is a duplicate,
// based on the type of cache request (.new, .update, .exp_update, .delete) and the existing data.
// Returns true to indicate a duplicate request, which will be ignored, and false otherwise.
//
-fn (tm DynamicTemplateManager) chandler_prevent_cache_duplicate_request(tc &TemplateCache) bool {
+fn (tm &DynamicTemplateManager) chandler_prevent_cache_duplicate_request(tc &TemplateCache) bool {
match tc.cache_request {
.new {
for value in tm.template_caches {
@@ -878,19 +944,24 @@ fn (tm DynamicTemplateManager) chandler_prevent_cache_duplicate_request(tc &Temp
// fn (mut DynamicTemplateManager) chandler_clear_specific_cache(int) return (int, bool)
//
-// Exclusively associated with the cache handler, is used to remove specific cache information from the database when necessary.
+// Used to remove specific rendered-cache information from the database when necessary.
// It identifies the target 'TemplateCache' by its id in the array database and deletes its corresponding cache file in 'disk mode'.
//
fn (mut tm DynamicTemplateManager) chandler_clear_specific_cache(id int) (int, bool) {
for key, value in tm.template_caches {
if value.id == id {
+ path_hash := fnv1a.sum64_string(value.path)
+ if active_cache_id := tm.template_cache_ids_by_path_hash[path_hash] {
+ if active_cache_id == id {
+ tm.template_cache_ids_by_path_hash.delete(path_hash)
+ }
+ }
match value.cache_storage_mode {
.memory {}
.disk {
- file_path := os.join_path(tm.template_cache_folder,
- '${value.name}_${value.checksum}.cache')
+ file_path := tm.cache_disk_path(value.name, value.checksum)
os.rm(file_path) or {
- eprintln('${message_signature_error} While deleting the specific cache file: ${file_path}, cache server will be stopped, you need to fix and restart application: : ${err.msg()}')
+ eprintln('${message_signature_error} While deleting the specific cache file: ${file_path}: ${err.msg()}')
break
}
}
@@ -904,7 +975,7 @@ fn (mut tm DynamicTemplateManager) chandler_clear_specific_cache(id int) (int, b
// fn (mut DynamicTemplateManager) chandler_remaining_cache_template_used(CacheRequest, int, int) return bool
//
-// Exclusively associated with the cache handler for managing the lifecycle of cache requests in 'nbr_of_remaining_template_request'.
+// Manages the lifecycle of rendered-cache requests in 'nbr_of_remaining_template_request'.
// For each cache request (creation, update, or deletion), it updates the status of ongoing requests and decides on necessary actions.
// The function returns a boolean value: if true, it authorizes the destruction of the expired cache, ensuring that it's only removed when no longer in use.
// If false, it indicates that the cache cannot yet be destroyed due to ongoing usage.
@@ -921,19 +992,23 @@ fn (mut tm DynamicTemplateManager) chandler_remaining_cache_template_used(cr Cac
.update, .exp_update {
// Marks the old cache as obsolete and adds a request for the new cache.
// If the old cache is no longer in use, it is immediately deleted. Otherwise, a flag is set for deferred deletion of the cache as soon as feasible
- for key, mut value in tm.nbr_of_remaining_template_request {
- if value.id == old_id {
- value.need_to_delete = true
- tm.nbr_of_remaining_template_request << RemainingTemplateRequest{
- id: id
- }
+ for key := 0; key < tm.nbr_of_remaining_template_request.len; key++ {
+ if tm.nbr_of_remaining_template_request[key].id == old_id {
+ tm.nbr_of_remaining_template_request[key].need_to_delete = true
// If possible, immediately deleted request
- if value.nbr_of_remaining_request <= 0 && value.need_to_delete == true {
+ if tm.nbr_of_remaining_template_request[key].nbr_of_remaining_request <= 0
+ && tm.nbr_of_remaining_template_request[key].need_to_delete == true {
tm.nbr_of_remaining_template_request.delete(key)
+ tm.nbr_of_remaining_template_request << RemainingTemplateRequest{
+ id: id
+ }
return true
// else, set for deferred deletion of the cache as soon as feasible
} else {
- value.need_to_send_delete_request = true
+ tm.nbr_of_remaining_template_request[key].need_to_send_delete_request = true
+ tm.nbr_of_remaining_template_request << RemainingTemplateRequest{
+ id: id
+ }
}
break
}
@@ -942,12 +1017,12 @@ fn (mut tm DynamicTemplateManager) chandler_remaining_cache_template_used(cr Cac
}
.delete {
// Removes the cache request from the list if it's no longer in use.
- for key, value in tm.nbr_of_remaining_template_request {
- if value.id == id {
- if value.nbr_of_remaining_request <= 0 && value.need_to_delete == true {
+ for key := 0; key < tm.nbr_of_remaining_template_request.len; key++ {
+ if tm.nbr_of_remaining_template_request[key].id == id {
+ if tm.nbr_of_remaining_template_request[key].nbr_of_remaining_request <= 0
+ && tm.nbr_of_remaining_template_request[key].need_to_delete == true {
tm.nbr_of_remaining_template_request.delete(key)
}
-
break
}
}
@@ -955,6 +1030,7 @@ fn (mut tm DynamicTemplateManager) chandler_remaining_cache_template_used(cr Cac
else {}
}
}
+
return true
}
@@ -983,21 +1059,36 @@ fn (mut tm DynamicTemplateManager) parse_tmpl_file(file_path string, tmpl_name s
// Performs a light compression of the HTML output by removing usless spaces, newlines, and tabs if user selected this option.
if is_compressed && tmpl_type == TemplateType.html && tmpl_ != internat_server_error {
- tmpl_ = tmpl_.replace_each(['\n', '', '\t', '', ' ', ' '])
- mut r := regex.regex_opt(r'>(\s+)<') or {
- tm.stop_cache_handler()
- eprintln('${message_signature_error} with regular expression for HTML light compression in parse_tmpl_file() function. Please check the syntax of the regex pattern : ${err.msg()}')
- return internat_server_error
- }
- tmpl_ = r.replace(tmpl_, '><')
- for tmpl_.contains(' ') {
- tmpl_ = tmpl_.replace(' ', ' ')
- }
+ tmpl_ = compress_html_output(tmpl_)
}
return tmpl_
}
+fn compress_html_output(html string) string {
+ mut compressed := html.replace('\n', '').replace('\t', '')
+ for compressed.contains(' ') {
+ compressed = compressed.replace(' ', ' ')
+ }
+ mut result := strings.new_builder(compressed.len)
+ mut i := 0
+ for i < compressed.len {
+ result.write_u8(compressed[i])
+ if compressed[i] == `>` {
+ mut j := i + 1
+ for j < compressed.len && compressed[j] == ` ` {
+ j++
+ }
+ if j < compressed.len && compressed[j] == `<` {
+ i = j
+ continue
+ }
+ }
+ i++
+ }
+ return result.str()
+}
+
// fn check_if_cache_delay_iscorrect(i64, string) return !
//
// Validates the user-specified cache expiration delay for templates.
@@ -1020,23 +1111,21 @@ fn check_if_cache_delay_iscorrect(cde i64, tmpl_name string) ! {
// to decide whether to create a new cache, update an existing or delivered a valid cache content.
//
fn (mut tm DynamicTemplateManager) cache_request_route(is_cache_exist bool, neg_cache_delay_expiration i64,
- last_template_mod i64, test_current_template_mod i64, cache_del_exp i64, gen_at i64, c_time i64, content_checksum string,
- current_content_checksum string) (CacheRequest, i64) {
+ last_template_mod i64, test_current_template_mod i64, cache_del_exp i64, gen_at i64, c_time i64,
+ content_checksum string, current_content_checksum string) (CacheRequest, i64) {
+ current_ts := if c_time > 0 { c_time } else { tm.next_cache_timestamp() }
if !is_cache_exist || neg_cache_delay_expiration == -1 {
// Require cache creation
- unique_ts := get_current_unix_micro_timestamp()
- tm.c_time = unique_ts
+ unique_ts := tm.next_cache_timestamp()
return CacheRequest.new, unique_ts
} else if last_template_mod < test_current_template_mod
|| content_checksum != current_content_checksum {
// Requires cache update as the template has been modified since the last time. it can be the template itself or its dynamic content.
- unique_ts := get_current_unix_micro_timestamp()
- tm.c_time = unique_ts
+ unique_ts := tm.next_cache_timestamp()
return CacheRequest.update, unique_ts
- } else if cache_del_exp != 0 && (gen_at + cache_del_exp) < tm.c_time {
- unique_ts := get_current_unix_micro_timestamp()
- tm.c_time = unique_ts
+ } else if cache_del_exp != 0 && (gen_at + cache_del_exp) < current_ts {
// Requires cache update as the cache expiration delay has elapsed.
+ unique_ts := tm.next_cache_timestamp()
return CacheRequest.exp_update, unique_ts
} else {
// Returns valid cached content, no update or creation necessary.
@@ -1044,59 +1133,14 @@ fn (mut tm DynamicTemplateManager) cache_request_route(is_cache_exist bool, neg_
}
}
-// fn (mut tm DynamicTemplateManager) handle_dtm_clock()
-//
-// Manages the internal clock. It periodically updates ( 4 minutes minimum, if there has been a recent update elsewhere in the system, it will be taken into account by the clock handler. )
-// The goal is ensuring that the DTM's internal time is maintained, especially during prolonged periods of inactivity on the website,
-// this can potentially lead to cache expiration issues if there is zero traffic for a while."
-//
-
-// Minimum update interval ( in seconds ) set to 4 minutes minimum_wait_time_until_next_update
-const update_duration = 240
-
-fn (mut tm DynamicTemplateManager) handle_dtm_clock() {
- mut need_to_close := false
- defer {
- tm.ch_stop_dtm_clock.close()
- eprintln('${message_signature_info} DTM clock handler has been successfully stopped.')
- }
-
- for {
- // Calculate the remaining time until the next update.
- current_time := get_current_unix_micro_timestamp() / convert_seconds
- mut time_since_last_update := int(current_time - (tm.c_time / convert_seconds))
- mut minimum_wait_time_until_next_update := update_duration
-
- // Update DTM clock if update interval exceeded otherwise, set next check based on time since last update
- if time_since_last_update >= update_duration {
- tm.c_time = current_time * convert_seconds
- } else {
- if time_since_last_update < 0 {
- time_since_last_update = 0
- }
- minimum_wait_time_until_next_update = (update_duration - time_since_last_update) +
- update_duration
- }
-
- // Wait until the next update interval or until a stop signal is received.
- for elapsed_time := 0; elapsed_time < minimum_wait_time_until_next_update; elapsed_time++ {
- select {
- _ := <-tm.ch_stop_dtm_clock {
- need_to_close = true
- break
- }
- else {
- // Attendre une seconde
- time.sleep(1 * time.second)
- }
- }
- }
- if need_to_close {
- break
- }
- // Reset wait time for next cycle.
- minimum_wait_time_until_next_update = update_duration
+fn (mut tm DynamicTemplateManager) next_cache_timestamp() i64 {
+ current_ts := get_current_unix_micro_timestamp()
+ if current_ts <= tm.c_time {
+ tm.c_time++
+ } else {
+ tm.c_time = current_ts
}
+ return tm.c_time
}
// fn get_current_unix_timestamp() return i64
diff --git a/vlib/x/templating/dtm/dynamic_template_manager_behavior_test.v b/vlib/x/templating/dtm/dynamic_template_manager_behavior_test.v
new file mode 100644
index 00000000000000..e776f81a6395e9
--- /dev/null
+++ b/vlib/x/templating/dtm/dynamic_template_manager_behavior_test.v
@@ -0,0 +1,333 @@
+// vtest retry: 3
+module dtm
+
+import os
+import time
+
+const behavior_temp_dtm_dir = 'dynamic_template_manager_behavior_test'
+const behavior_temp_cache_dir = 'vcache_dtm'
+const behavior_temp_templates_dir = 'templates'
+const behavior_vtmp_dir = os.vtmp_dir()
+
+fn behavior_test_root_dir() string {
+ return os.join_path(behavior_vtmp_dir, '${behavior_temp_dtm_dir}_${os.getpid()}')
+}
+
+fn testsuite_begin() {
+ temp_folder := behavior_test_root_dir()
+ os.rmdir_all(temp_folder) or {}
+ os.mkdir_all(os.join_path(temp_folder, behavior_temp_cache_dir))!
+ os.mkdir_all(os.join_path(temp_folder, behavior_temp_templates_dir))!
+
+ templates_path := os.join_path(temp_folder, behavior_temp_templates_dir)
+ os.write_file(os.join_path(templates_path, 'page.html'), [
+ '',
+ '
')
+}
+
+fn test_expand_resolves_relative_includes() {
+ mut dtmi := new_behavior_dtm(false, false, max_size_data_in_memory)
+ placeholders := {
+ 'title': DtmMultiTypeMap('Included ')
+ }
+
+ rendered := dtmi.expand(behavior_template_path('layout.html'), placeholders: &placeholders)
+
+ assert rendered.contains('Start')
+ assert rendered.contains('Included <Title>')
+ assert rendered.contains('')
+}
+
+fn test_includehtml_preserves_allowed_tags_and_escapes_unlisted_tags() {
+ mut dtmi := new_behavior_dtm(false, false, max_size_data_in_memory)
+ placeholders := {
+ 'content_#includehtml': DtmMultiTypeMap('allowed')
+ }
+
+ rendered :=
+ dtmi.expand(behavior_template_path('include_html.html'), placeholders: &placeholders)
+
+ assert rendered.contains('allowed<script>blocked()</script>')
+}
+
+fn test_includehtml_is_still_escaped_in_text_templates() {
+ mut dtmi := new_behavior_dtm(false, false, max_size_data_in_memory)
+ placeholders := {
+ 'content_#includehtml': DtmMultiTypeMap('text')
+ }
+
+ rendered := dtmi.expand(behavior_template_path('include_text.txt'), placeholders: &placeholders)
+
+ assert rendered.contains('Text: <span>text</span>')
+}
+
+fn test_expand_returns_internal_error_for_missing_template() {
+ mut dtmi := new_behavior_dtm(false, false, max_size_data_in_memory)
+ placeholders := map[string]DtmMultiTypeMap{}
+
+ rendered := dtmi.expand(behavior_template_path('missing.html'), placeholders: &placeholders)
+
+ assert rendered == internat_server_error
+}
+
+fn test_expand_returns_internal_error_for_invalid_template_extension() {
+ mut dtmi := new_behavior_dtm(false, false, max_size_data_in_memory)
+ placeholders := map[string]DtmMultiTypeMap{}
+
+ rendered := dtmi.expand(behavior_template_path('invalid.css'), placeholders: &placeholders)
+
+ assert rendered == internat_server_error
+}
+
+fn test_expand_without_cache_renders_fresh_placeholder_content() {
+ mut dtmi := new_behavior_dtm(false, false, max_size_data_in_memory)
+ first_placeholders := {
+ 'title': DtmMultiTypeMap('first')
+ }
+ second_placeholders := {
+ 'title': DtmMultiTypeMap('second')
+ }
+
+ first := dtmi.expand(behavior_template_path('cache.html'), placeholders: &first_placeholders)
+ second := dtmi.expand(behavior_template_path('cache.html'), placeholders: &second_placeholders)
+
+ assert first.contains('first')
+ assert second.contains('second')
+}
+
+fn test_expand_with_cache_updates_when_placeholder_content_changes() {
+ mut dtmi := new_behavior_dtm(true, false, max_size_data_in_memory)
+ defer {
+ dtmi.disable_cache()
+ }
+ first_placeholders := {
+ 'title': DtmMultiTypeMap('cached first')
+ }
+ second_placeholders := {
+ 'title': DtmMultiTypeMap('cached second')
+ }
+
+ first := dtmi.expand(behavior_template_path('cache.html'), placeholders: &first_placeholders)
+ second := dtmi.expand(behavior_template_path('cache.html'), placeholders: &first_placeholders)
+ third := dtmi.expand(behavior_template_path('cache.html'), placeholders: &second_placeholders)
+
+ assert first.contains('cached first')
+ assert second == first
+ assert third.contains('cached second')
+}
+
+fn test_expand_with_legacy_cache_enabled_uses_new_parsed_template_engine() {
+ mut dtmi := new_behavior_dtm(true, false, max_size_data_in_memory)
+ defer {
+ dtmi.disable_cache()
+ }
+ placeholders := {
+ 'title': DtmMultiTypeMap('stable cached value')
+ }
+
+ first := dtmi.expand(behavior_template_path('cache.html'), placeholders: &placeholders)
+ second := dtmi.expand(behavior_template_path('cache.html'), placeholders: &placeholders)
+
+ assert first == second
+ assert rendered_cache_count(dtmi) == 0
+ assert dtmi.render_engine.compiled_template_count() == 1
+}
+
+fn test_expand_with_cache_updates_when_template_file_changes() {
+ mut dtmi := new_behavior_dtm(true, false, max_size_data_in_memory)
+ defer {
+ dtmi.disable_cache()
+ }
+ template_path := behavior_template_path('cache_file_update.html')
+ placeholders := {
+ 'title': DtmMultiTypeMap('file driven')
+ }
+
+ first := dtmi.expand(template_path, placeholders: &placeholders)
+ time.sleep(1100 * time.millisecond)
+ os.write_file(template_path, '@title')!
+ second := dtmi.expand(template_path, placeholders: &placeholders)
+
+ assert first.contains('file driven')
+ assert second.contains('file driven')
+}
+
+fn test_cache_delay_minus_one_skips_rendered_cache_storage() {
+ mut dtmi := new_behavior_dtm(true, false, max_size_data_in_memory)
+ defer {
+ dtmi.disable_cache()
+ }
+ placeholders := {
+ 'title': DtmMultiTypeMap('no store')
+ }
+
+ rendered := dtmi.expand(behavior_template_path('no_store.html'),
+ placeholders: &placeholders
+ cache_delay_expiration: -1
+ )
+
+ assert rendered.contains('no store')
+ assert rendered_cache_count(dtmi) == 0
+}
+
+fn test_cache_delay_zero_is_accepted_without_rendered_cache_storage() {
+ mut dtmi := new_behavior_dtm(true, false, max_size_data_in_memory)
+ defer {
+ dtmi.disable_cache()
+ }
+ placeholders := {
+ 'title': DtmMultiTypeMap('forever')
+ }
+
+ rendered := dtmi.expand(behavior_template_path('forever.html'),
+ placeholders: &placeholders
+ cache_delay_expiration: 0
+ )
+
+ assert rendered.contains('forever')
+ assert rendered_cache_count(dtmi) == 0
+}
+
+fn test_memory_limit_zero_does_not_create_rendered_disk_cache() {
+ mut dtmi := new_behavior_dtm(true, false, 0)
+ defer {
+ dtmi.disable_cache()
+ }
+ placeholders := {
+ 'title': DtmMultiTypeMap('disk mode')
+ }
+
+ rendered := dtmi.expand(behavior_template_path('disk_cache.html'), placeholders: &placeholders)
+
+ assert rendered.contains('disk mode')
+ assert rendered_cache_count(dtmi) == 0
+ for file_name in os.ls(dtmi.template_cache_folder)! {
+ assert !file_name.ends_with('.cache')
+ }
+}
diff --git a/vlib/x/templating/dtm/dynamic_template_manager_cache_system_test.v b/vlib/x/templating/dtm/dynamic_template_manager_cache_system_test.v
index 031598c45ed053..aec84fa27c5690 100644
--- a/vlib/x/templating/dtm/dynamic_template_manager_cache_system_test.v
+++ b/vlib/x/templating/dtm/dynamic_template_manager_cache_system_test.v
@@ -62,12 +62,10 @@ fn test_get_cache() {
defer {
dtmi.stop_cache_handler()
}
- if !dtmi.abort_test {
- dtm_placeholders := map[string]DtmMultiTypeMap{}
- temp_html_file := os.join_path(dtmi.template_folder, temp_html_fp)
- html_mem := dtmi.get_cache(temp_html_n, temp_html_file, &dtm_placeholders)
- assert html_mem.len > 10
- }
+ dtm_placeholders := map[string]DtmMultiTypeMap{}
+ temp_html_file := os.join_path(dtmi.template_folder, temp_html_fp)
+ html_mem := dtmi.get_cache(temp_html_n, temp_html_file, &dtm_placeholders)
+ assert html_mem.len > 10
}
fn test_chandler_clear_specific_cache() {
@@ -76,20 +74,18 @@ fn test_chandler_clear_specific_cache() {
dtmi.stop_cache_handler()
}
dtmi.create_cache()
- if !dtmi.abort_test {
- lock dtmi.template_caches {
- cache_file := os.join_path(dtmi.template_cache_folder,
- '${dtmi.template_caches[0].name}_${dtmi.template_caches[0].checksum}.cache')
- index, is_success := dtmi.chandler_clear_specific_cache(dtmi.template_caches[0].id)
- assert is_success == true
- assert index == 0
- cache_exist := os.exists(cache_file)
- assert cache_exist == false
- }
+ lock dtmi.template_caches {
+ cache_file := os.join_path(dtmi.template_cache_folder,
+ '${dtmi.template_caches[0].name}_${dtmi.template_caches[0].checksum}.cache')
+ index, is_success := dtmi.chandler_clear_specific_cache(dtmi.template_caches[0].id)
+ assert is_success == true
+ assert index == 0
+ cache_exist := os.exists(cache_file)
+ assert cache_exist == false
}
}
-fn test_handle_dtm_clock() {
+fn test_cache_timestamp_is_initialized() {
mut dtmi := init_dtm(true, max_size_data_in_memory)
defer {
dtmi.stop_cache_handler()
@@ -98,30 +94,26 @@ fn test_handle_dtm_clock() {
assert date_to_str.len > 10
}
-fn test_cache_handler() {
+fn test_process_rendered_cache_delete_request() {
mut dtmi := init_dtm(true, max_size_data_in_memory)
defer {
dtmi.stop_cache_handler()
}
dtmi.create_cache()
- if !dtmi.abort_test {
- path_f := os.join_path(dtmi.template_folder, temp_html_fp)
- lock dtmi.template_caches {
- assert dtmi.template_caches[0].id == 1
- assert dtmi.template_caches[0].name == temp_html_n
- assert dtmi.template_caches[0].path == path_f
- }
- dtmi.id_to_handlered = 1
- dtmi.ch_cache_handler <- TemplateCache{
- id: 1
- cache_request: .delete
- }
- dtmi.sync_cache()
- if !dtmi.abort_test {
- lock dtmi.template_caches {
- assert dtmi.template_caches.len == 0
- }
- }
+ path_f := os.join_path(dtmi.template_folder, temp_html_fp)
+ lock dtmi.template_caches {
+ assert dtmi.template_caches[0].id == 1
+ assert dtmi.template_caches[0].name == temp_html_n
+ assert dtmi.template_caches[0].path == path_f
+ }
+ dtmi.id_to_handlered = 1
+ dtmi.ch_cache_handler <- TemplateCache{
+ id: 1
+ cache_request: .delete
+ }
+ dtmi.sync_cache()
+ rlock dtmi.template_caches {
+ assert dtmi.template_caches.len == 0
}
}
diff --git a/vlib/x/templating/dtm/dynamic_template_manager_dtm2_bridge.v b/vlib/x/templating/dtm/dynamic_template_manager_dtm2_bridge.v
new file mode 100644
index 00000000000000..dd05f9ef714955
--- /dev/null
+++ b/vlib/x/templating/dtm/dynamic_template_manager_dtm2_bridge.v
@@ -0,0 +1,65 @@
+module dtm
+
+import x.templating.dtm2
+
+// new_dtm2_render_engine is the compatibility boundary between the historical
+// DTM API and the DTM2 runtime template engine. DTM2 remains unaware
+// of legacy cache concepts; it only parses templates and renders them.
+fn new_dtm2_render_engine(template_dir string, compress_html bool) &dtm2.Manager {
+ return dtm2.initialize(
+ template_dir: template_dir
+ compress_html: compress_html
+ reload_modified_templates: true
+ )
+}
+
+fn (mut tm DynamicTemplateManager) expand_with_dtm2_render_engine(tmpl_path string, tmpl_var TemplateCacheParams) string {
+ validate_legacy_placeholder_sizes(tmpl_path, tmpl_var.placeholders) or {
+ eprintln(err.msg())
+ return internat_server_error
+ }
+ if isnil(tm.render_engine) {
+ tm.render_engine = new_dtm2_render_engine(tm.template_folder, tm.compress_html)
+ }
+ placeholders := convert_legacy_placeholders(tmpl_var.placeholders)
+ return tm.render_engine.expand(tmpl_path,
+ placeholders: &placeholders
+ missing_placeholder_prefix: '$'
+ )
+}
+
+fn validate_legacy_placeholder_sizes(tmpl_path string, placeholders &map[string]DtmMultiTypeMap) ! {
+ for key, value in placeholders {
+ if key.len > max_placeholders_key_size {
+ return error('${message_signature_error} Length of placeholder key "${key}" exceeds the maximum allowed size for template content in file: ${tmpl_path}. Max allowed size: ${max_placeholders_key_size} characters.')
+ }
+ casted_value := legacy_placeholder_value_to_string(*value)
+ if casted_value.len > max_placeholders_value_size {
+ return error('${message_signature_error} Length of placeholder value for key "${key}" exceeds the maximum allowed size for template content in file: ${tmpl_path}. Max allowed size: ${max_placeholders_value_size} characters.')
+ }
+ }
+}
+
+fn convert_legacy_placeholders(placeholders &map[string]DtmMultiTypeMap) map[string]string {
+ mut converted := map[string]string{}
+ for key, value in placeholders {
+ converted[key.clone()] = legacy_placeholder_value_to_string(*value).clone()
+ }
+ return converted
+}
+
+fn legacy_placeholder_value_to_string(value DtmMultiTypeMap) string {
+ return match value {
+ f32 { value.str() }
+ f64 { value.str() }
+ i16 { value.str() }
+ i64 { value.str() }
+ i8 { value.str() }
+ int { value.str() }
+ string { value.clone() }
+ u16 { value.str() }
+ u32 { value.str() }
+ u64 { value.str() }
+ u8 { value.str() }
+ }
+}
diff --git a/vlib/x/templating/dtm/dynamic_template_manager_test.v b/vlib/x/templating/dtm/dynamic_template_manager_test.v
index 28744d3569f636..f2cdf6b1471cc9 100644
--- a/vlib/x/templating/dtm/dynamic_template_manager_test.v
+++ b/vlib/x/templating/dtm/dynamic_template_manager_test.v
@@ -160,7 +160,7 @@ fn test_check_tmpl_and_placeholders_size() {
}
fn test_chandler_prevent_cache_duplicate_request() {
- dtmi := init_dtm(false, 0)
+ mut dtmi := init_dtm(false, 0)
temp_html_file := os.join_path(dtmi.template_folder, temp_html_fp)
lock dtmi.template_caches {
diff --git a/vlib/x/templating/dtm/escape_html_strings_in_templates.v b/vlib/x/templating/dtm/escape_html_strings_in_templates.v
index fbe88a3640093e..6819c449472db5 100644
--- a/vlib/x/templating/dtm/escape_html_strings_in_templates.v
+++ b/vlib/x/templating/dtm/escape_html_strings_in_templates.v
@@ -1,7 +1,30 @@
module dtm
-import encoding.html
+import strings
fn filter(s string) string {
- return html.escape(s)
+ mut escaped := strings.new_builder(s.len)
+ for i := 0; i < s.len; i++ {
+ match s[i] {
+ `&` {
+ escaped.write_string('&')
+ }
+ `<` {
+ escaped.write_string('<')
+ }
+ `>` {
+ escaped.write_string('>')
+ }
+ `"` {
+ escaped.write_string('"')
+ }
+ `'` {
+ escaped.write_string(''')
+ }
+ else {
+ escaped.write_u8(s[i])
+ }
+ }
+ }
+ return escaped.str()
}
diff --git a/vlib/x/templating/dtm/tmpl.v b/vlib/x/templating/dtm/tmpl.v
index 1c8a91a2717c0c..fee8905d069ab5 100644
--- a/vlib/x/templating/dtm/tmpl.v
+++ b/vlib/x/templating/dtm/tmpl.v
@@ -79,6 +79,9 @@ fn is_html_open_tag(name string, s string) bool {
fn replace_placeholders_with_data(line string, data &map[string]DtmMultiTypeMap, state State) string {
mut rline := line
+ if data.len == 0 {
+ return rline
+ }
mut need_include_html := false
for key, value in data {
@@ -104,12 +107,13 @@ fn replace_placeholders_with_data(line string, data &map[string]DtmMultiTypeMap,
// Checks if the placeholder allows HTML inclusion
if need_include_html {
if state == State.html {
- // Iterates over allowed HTML tags for inclusion
+ // Escape the whole value first, then restore only the explicit allow-list.
+ // This preserves the documented opt-in HTML behavior without allowing
+ // arbitrary raw tags through `_#includehtml`.
+ val_str = filter(value)
for tag in allowed_tags {
- // Escapes the HTML tag
escaped_tag := filter(tag)
- // Replaces the escaped tags with actual HTML tags in the value
- val_str = value.replace(escaped_tag, tag)
+ val_str = val_str.replace(escaped_tag, tag)
}
} else {
val_str = filter(value)
@@ -124,10 +128,6 @@ fn replace_placeholders_with_data(line string, data &map[string]DtmMultiTypeMap,
rline = rline.replace(placeholder, val_str)
}
- // If no output is found for the placeholder being processed, then the placeholder is escaped
- if rline.contains('$') {
- rline = filter(rline)
- }
return rline
}
@@ -136,9 +136,8 @@ fn insert_template_code(fn_name string, tmpl_str_start string, line string, data
// HTML, may include `@var`
// escaped by cgen, unless it's a `veb.RawHtml` string
trailing_bs := tmpl_str_end + 'sb_${fn_name}.write_u8(92)\n' + tmpl_str_start
- round1 := ['\\', '\\\\', r"'", "\\'", r'@', r'$']
- round2 := [r'$$', r'\@', r'.$', r'.@']
- mut rline := line.replace_each(round1).replace_each(round2)
+ mut rline := line.replace('\\', '\\\\').replace("'", "\\'").replace('@', '$')
+ rline = rline.replace(r'$$', r'\@').replace(r'.$', r'.@')
comptime_call_str := rline.find_between('\${', '}')
if comptime_call_str.contains("\\'") {
rline = rline.replace(comptime_call_str, comptime_call_str.replace("\\'", r"'"))
@@ -154,10 +153,11 @@ fn insert_template_code(fn_name string, tmpl_str_start string, line string, data
// compile_file compiles the content of a file by the given path as a template
fn compile_template_file(template_file string, fn_name string, data &map[string]DtmMultiTypeMap) string {
- mut lines := os.read_lines(template_file) or {
+ template_content := os.read_file(template_file) or {
eprintln('${message_signature_error} Template generator can not reading from ${template_file} file')
return internat_server_error
}
+ mut lines := template_content.split_into_lines()
basepath := os.dir(template_file)
diff --git a/vlib/x/templating/dtm2/README.md b/vlib/x/templating/dtm2/README.md
new file mode 100644
index 00000000000000..7e2f682db3b134
--- /dev/null
+++ b/vlib/x/templating/dtm2/README.md
@@ -0,0 +1,365 @@
+# dtm2 - Dynamic Template Manager 2
+
+`dtm2` is the modern runtime renderer for Dynamic Template Manager.
+It keeps the original DTM idea: templates are normal files on disk, so they can
+be edited without recompiling the application.
+
+The main change from `x.templating.dtm` is architectural. `dtm2` caches parsed
+template trees, not rendered HTML responses. This keeps rendering fast while
+removing the old async rendered-cache server from the hot path.
+
+## Quick Start
+
+Create a `templates/` folder in your application and put your templates inside
+it. DTM2 supports `.html`, `.htm`, `.xml`, `.txt`, and `.text` by default.
+
+```v
+import x.templating.dtm2
+
+fn main() {
+ mut manager := dtm2.initialize(
+ template_dir: 'templates'
+ )
+ placeholders := {
+ 'title': 'DTM2'
+ 'body': 'escaped by default'
+ }
+ rendered := manager.expand('page.html', placeholders: &placeholders)
+ println(rendered)
+}
+```
+
+Example template:
+
+```html
+
+
+
+ @title
+
+
+ @body
+
+
+```
+
+The rendered `@body` value is escaped by default.
+
+## Veb Example
+
+```v
+import veb
+import x.templating.dtm2
+
+pub struct App {
+pub mut:
+ templates &dtm2.Manager = unsafe { nil }
+}
+
+pub struct Context {
+ veb.Context
+}
+
+fn main() {
+ mut app := &App{
+ templates: dtm2.initialize(
+ template_dir: 'templates'
+ )
+ }
+ veb.run[App, Context](mut app, 18081)
+}
+
+@['/']
+pub fn (mut app App) index(mut ctx Context) veb.Result {
+ placeholders := {
+ 'title': 'Home'
+ 'body': 'Hello from DTM2'
+ }
+ html := app.templates.expand('index.html', placeholders: &placeholders)
+ return ctx.html(html)
+}
+```
+
+## Available Options
+
+### Manager options
+
+`dtm2.initialize()` accepts:
+
+- `template_dir` (**string**): root directory used for relative template paths.
+ If empty, `/templates` is used.
+- `compress_html` (**bool**): enables a lightweight deterministic HTML
+ whitespace compressor. It is enabled by default.
+- `reload_modified_templates` (**bool**): when enabled, DTM2 checks the source
+ template and included files before reusing a parsed template tree. It is
+ enabled by default.
+- `extension_config_file` (**string**): optional JSON file containing extension
+ mappings. If empty, DTM2 automatically loads `dtm2_extensions.json` from the
+ configured `template_dir` when that file exists.
+
+Example:
+
+```v ignore
+mut manager := dtm2.initialize(
+ template_dir: 'templates'
+ compress_html: true
+ reload_modified_templates: true
+)
+```
+
+For maximum hot-path throughput in applications where templates are immutable
+after startup, you can disable reload checks:
+
+```v ignore
+mut manager := dtm2.initialize(
+ template_dir: 'templates'
+ reload_modified_templates: false
+)
+```
+
+The recommended runtime model is one long-lived manager per application or
+rendering context. Reusing the manager is what keeps parsed templates and path
+resolution cached.
+
+### Template extensions
+
+DTM2 has two rendering modes:
+
+- `TemplateType.html`: HTML/XML-like output, with default escaping and optional
+ HTML compression.
+- `TemplateType.text`: raw text output, also escaped by default.
+
+Default mappings:
+
+- HTML mode: `.html`, `.htm`, `.xml`
+- Text mode: `.txt`, `.text`
+
+Project-specific extensions should be configured with a JSON file.
+
+Example `templates/dtm2_extensions.json`:
+
+```json
+{
+ "html": [".view", ".tmpl"],
+ "text": [".mail", ".md"]
+}
+```
+
+If the file is named `dtm2_extensions.json` and is placed directly in the
+configured `template_dir`, DTM2 loads it automatically:
+
+```v ignore
+mut manager := dtm2.initialize(
+ template_dir: 'templates'
+)
+```
+
+You can also point to an explicit config file:
+
+```v ignore
+mut manager := dtm2.initialize(
+ template_dir: 'templates'
+ extension_config_file: 'config/my_dtm2_extensions.json'
+)
+```
+
+DTM2 ships a default config example that can be copied into your own
+`templates/` directory:
+
+```sh
+vlib/x/templating/dtm2/dtm2_extensions.json
+```
+
+If the JSON file is absent, invalid, or contains invalid entries, DTM2 keeps the
+built-in defaults and prints a warning for entries that cannot be registered.
+JSON extension config files are limited to 64 KB and every extension is
+validated before being registered.
+
+### Render options
+
+`manager.expand()` accepts:
+
+- `placeholders` (**&map[string]string**): values inserted in the template.
+- `missing_placeholder_prefix` (**string**): prefix written when a placeholder
+ is missing. The default is `@`, preserving the original placeholder text.
+
+Example:
+
+```v ignore
+placeholders := {
+ 'title': 'Example'
+}
+html := manager.expand('page.html',
+ placeholders: &placeholders
+ missing_placeholder_prefix: '@'
+)
+```
+
+## The Placeholder System
+
+Template placeholders use the `@name` form.
+
+```html
+
@title
+
@body
+```
+
+In DTM2, placeholder values are strings:
+
+```v ignore
+placeholders := {
+ 'title': 'Hello'
+ 'count': '42'
+}
+```
+
+Values are escaped by default in both HTML and text templates.
+
+**Security note for custom non-HTML formats such as SQL:** DTM2 is a template
+renderer, not a domain-specific sanitizer. By default, it escapes HTML-special
+characters in placeholder values. It does not make SQL queries safe, does not
+replace prepared statements, and does not validate business-specific formats.
+If you add `.sql` or another sensitive extension through configuration, the
+security of that generated content remains the responsibility of the
+application.
+
+## Explicit HTML Inclusion
+
+The historical `_#includehtml` suffix is still supported for compatibility.
+It allows a placeholder to include a restricted set of HTML tags in `.html`
+templates.
+
+```v ignore
+placeholders := {
+ 'body_#includehtml': '
allowed
'
+}
+html := manager.expand('page.html', placeholders: &placeholders)
+```
+
+The template still uses the normal placeholder name:
+
+```html
+@body
+```
+
+DTM2 escapes the complete value first, then restores only allowed tags. This
+preserves the old opt-in behavior without allowing arbitrary raw HTML through.
+In `.txt` templates, HTML is always escaped.
+
+Allowed tags:
+
+```html
+
,
,
,
,
,
,
,
,
,
,
+
,
,
,
,
,
, , , , ,
+
,
, , ,
,
,
,
,
,
,
+
,
, ,
,
,
,
,
+
,
,
,
,
,
, , ,
+, , , , , ,
, ,
+
,
, , , ,
+, , , , , ,
+, , , ,
+, , ,
+```
+
+## Includes
+
+Templates can include other templates with a simple line-level directive:
+
+```html
+@include "partials/nav"
+@body
+```
+
+Include paths are resolved relative to the current template. If no file
+extension is provided, `.html` is added. The final resolved include path must
+stay inside the manager `template_dir`; attempts to include files through `../`
+or absolute paths outside that root fail.
+
+The same boundary applies to templates passed to `expand()`. Absolute template
+paths are accepted only when they resolve inside `template_dir`.
+
+Included files are tracked as dependencies of the parsed template. When
+`reload_modified_templates` is enabled, changing an included file invalidates
+the cached parsed tree.
+
+## Backward Compatibility With DTM v1
+
+Existing code that imports `x.templating.dtm` is kept source-compatible for the
+migration period. The v1 facade is deprecated, but it now delegates rendering to
+DTM2 internally.
+
+That means old code can continue to compile:
+
+```v ignore
+import x.templating.dtm
+
+mut manager := dtm.initialize()
+mut placeholders := map[string]dtm.DtmMultiTypeMap{}
+placeholders['title'] = 'Legacy DTM'
+placeholders['count'] = 7
+html := manager.expand('page.html', placeholders: &placeholders)
+```
+
+For new code, prefer importing `x.templating.dtm2` directly:
+
+```v ignore
+import x.templating.dtm2
+
+mut manager := dtm2.initialize(template_dir: 'templates')
+placeholders := {
+ 'title': 'Modern DTM'
+ 'count': '7'
+}
+html := manager.expand('page.html', placeholders: &placeholders)
+```
+
+Migration notes:
+
+- Replace `import x.templating.dtm` with `import x.templating.dtm2`.
+- Replace `DtmMultiTypeMap` placeholder maps with `map[string]string`.
+- Convert numeric values to strings before rendering.
+- Remove `stop_cache_handler()` calls; DTM2 does not start an async cache
+ server.
+- Keep using `_#includehtml` only when HTML inclusion is intentional.
+
+## Design Notes
+
+DTM2 intentionally keeps rendering and rendered-output caching separate.
+
+The manager caches:
+
+- canonical template paths;
+- parsed template trees;
+- dependency metadata for root templates and includes.
+
+The manager does not cache rendered HTML responses. If a future rendered-cache
+layer is needed, it should remain a small optional layer above DTM2 rather than
+part of the parser/renderer core.
+
+## Benchmarks
+
+The local benchmark harness lives in:
+
+```sh
+vlib/x/templating/dtm2/benchmarks/
+```
+
+Run it from the repository root:
+
+```sh
+vlib/x/templating/dtm2/benchmarks/run_dtm2_benchmark.sh
+```
+
+Useful options:
+
+- `DTM2_BENCH_MODE=prod|prod_o2|dev`
+- `DTM2_BENCH_CASE=all|small_hot|small_cold|many_hot|many_cold|include_hot|include_cold|xml_hot|xml_cold`
+- `DTM2_BENCH_ITERATIONS=50000`
+- `DTM2_BENCH_COLD_ITERATIONS=500`
+- `DTM2_BENCH_PLACEHOLDERS=50`
+- `DTM2_BENCH_COMPRESS_HTML=true`
+- `DTM2_BENCH_RELOAD_MODIFIED_TEMPLATES=false`
+- `DTM2_BENCH_VALIDATE_EACH_ITERATION=false`
+
+Benchmark result directories are generated locally under
+`vlib/x/templating/dtm2/benchmarks/results/` and should not be committed.
diff --git a/vlib/x/templating/dtm2/benchmarks/dtm2_benchmark.v b/vlib/x/templating/dtm2/benchmarks/dtm2_benchmark.v
new file mode 100644
index 00000000000000..e33bf586aeeec2
--- /dev/null
+++ b/vlib/x/templating/dtm2/benchmarks/dtm2_benchmark.v
@@ -0,0 +1,212 @@
+module main
+
+import os
+import strings
+import time
+import x.templating.dtm2
+
+const bench_root_name = 'dtm2_benchmark'
+
+struct BenchConfig {
+ case_name string
+ iterations int
+ cold_iterations int
+ placeholder_count int
+ compress_html bool
+ reload_modified_templates bool
+ validate_each_iteration bool
+}
+
+fn main() {
+ config := BenchConfig{
+ case_name: os.getenv('DTM2_BENCH_CASE')
+ iterations: env_int('DTM2_BENCH_ITERATIONS', 50000)
+ cold_iterations: env_int('DTM2_BENCH_COLD_ITERATIONS', 500)
+ placeholder_count: env_int('DTM2_BENCH_PLACEHOLDERS', 50)
+ compress_html: env_bool('DTM2_BENCH_COMPRESS_HTML', true)
+ reload_modified_templates: env_bool('DTM2_BENCH_RELOAD_MODIFIED_TEMPLATES', false)
+ validate_each_iteration: env_bool('DTM2_BENCH_VALIDATE_EACH_ITERATION', false)
+ }
+ root := os.join_path(os.vtmp_dir(), '${bench_root_name}_${os.getpid()}')
+ os.rmdir_all(root) or {}
+ setup_files(root, config)!
+ defer {
+ os.rmdir_all(root) or {}
+ }
+
+ print_config(root, config)
+ small := small_placeholders()
+ many := many_placeholders(config.placeholder_count)
+ include := include_placeholders()
+
+ if should_run(config, 'small_hot') {
+ bench_hot('small_hot', root, 'small.html', small, config.iterations, config)
+ }
+ if should_run(config, 'small_cold') {
+ bench_cold('small_cold', root, 'small.html', small, config.cold_iterations, config)
+ }
+ if should_run(config, 'many_hot') {
+ bench_hot('many_hot', root, 'many.html', many, config.iterations, config)
+ }
+ if should_run(config, 'many_cold') {
+ bench_cold('many_cold', root, 'many.html', many, config.cold_iterations, config)
+ }
+ if should_run(config, 'include_hot') {
+ bench_hot('include_hot', root, 'with_include.html', include, config.iterations, config)
+ }
+ if should_run(config, 'include_cold') {
+ bench_cold('include_cold', root, 'with_include.html', include, config.cold_iterations,
+ config)
+ }
+ if should_run(config, 'xml_hot') {
+ bench_hot('xml_hot', root, 'feed.xml', small, config.iterations, config)
+ }
+ if should_run(config, 'xml_cold') {
+ bench_cold('xml_cold', root, 'feed.xml', small, config.cold_iterations, config)
+ }
+}
+
+fn print_config(root string, config BenchConfig) {
+ println('DTM2 benchmark')
+ println('root: ${root}')
+ println('case_name: ${if config.case_name == '' { 'all' } else { config.case_name }}')
+ println('iterations: ${config.iterations}')
+ println('cold_iterations: ${config.cold_iterations}')
+ println('placeholder_count: ${config.placeholder_count}')
+ println('compress_html: ${config.compress_html}')
+ println('reload_modified_templates: ${config.reload_modified_templates}')
+ println('validate_each_iteration: ${config.validate_each_iteration}')
+ println('')
+}
+
+fn env_int(name string, default_value int) int {
+ raw := os.getenv(name)
+ if raw == '' {
+ return default_value
+ }
+ value := raw.int()
+ if value <= 0 {
+ return default_value
+ }
+ return value
+}
+
+fn env_bool(name string, default_value bool) bool {
+ raw := os.getenv(name).to_lower()
+ if raw == '' {
+ return default_value
+ }
+ if raw in ['1', 'true', 'yes', 'on'] {
+ return true
+ }
+ if raw in ['0', 'false', 'no', 'off'] {
+ return false
+ }
+ return default_value
+}
+
+fn should_run(config BenchConfig, label string) bool {
+ return config.case_name == '' || config.case_name == 'all' || config.case_name == label
+}
+
+fn setup_files(root string, config BenchConfig) ! {
+ os.mkdir_all(os.join_path(root, 'partials'))!
+ os.write_file(os.join_path(root, 'small.html'),
+ '@title
@body
@count')!
+ mut many := strings.new_builder(config.placeholder_count * 30)
+ many.writeln('
')
+ for i := 0; i < config.placeholder_count; i++ {
+ many.writeln('