API Reference
Everything exported by DynamicObjects. For usage and worked examples see the manual.
The struct macro
DynamicObjects.@dynamicstruct Macro
@dynamicstruct [docstring] struct Name
field # fixed field (constructor argument)
prop = expr # lazily computed property
@cached prop = expr # lazily computed + disk-cached property
prop(idx) = expr # indexable property (cached per args; `@fresh` to bypass)
prop(args...; kw...) = expr # indexable property (cached per args; `@fresh` to bypass)
@cached prop(idx) = expr # indexable + disk-cached property (cached per index)
endDefine a struct whose fixed fields are set at construction time and whose derived properties are computed lazily on first access and then stored in an in-memory cache.
Derived properties may reference any other field or property by name; the reference is automatically rewritten to __self__.<name>. Order of definition does not matter — cycles will result in a stack overflow at runtime.
The in-memory cache is always a ThreadsafeDict — safe to access from multiple tasks simultaneously; duplicate work is avoided by sharing in-flight Tasks.
Properties marked @cached are additionally persisted to disk under __self__.__cache_path__ (which itself defaults to joinpath(__self__.__cache_base__, __self__.__hash__)).
Keyword arguments passed to the constructor pre-populate the cache, so they act as overrides for any computed property.
Examples
using DynamicObjects
@dynamicstruct struct Point
x::Float64
y::Float64
r = sqrt(x^2 + y^2)
theta = atan(y, x)
end
p = Point(3.0, 4.0)
p.r # 5.0
p.theta # atan(4, 3)# Disk-cached expensive computation.
# cache_path defaults to joinpath("cache", hash(n)), so two Experiment(n)
# instances with the same n share the same cache directory.
@dynamicstruct struct Experiment
n::Int
@cached result = sum(rand(n)) # computed once, then loaded from disk
end
e = Experiment(1_000_000)
e.result # computed on first access, cached to disk
e2 = Experiment(1_000_000)
e2.result # loaded from disk (same n → same hash → same cache path)# Indexed properties. `obj.prop(args)` caches by default; `@fresh obj.prop(args)` recomputes.
# Properties reference each other by bare name (auto-rewritten to __self__.<name>).
@dynamicstruct struct DataSet
items = ["apple", "banana", "cherry"]
matches(query) = filter(x -> occursin(query, x), items) # call: cached per query
top(query; n=1) = first(matches(query), n) # call with kwargs
end
ds = DataSet()
ds.matches("an") # ["banana"] — cached in the per-property dict (keyed by args)
@fresh ds.matches("an") # ["banana"] — recomputed fresh, bypassing the cache
fresh(ds.matches, "an") # ["banana"] — explicit uncached call (outside a @dynamicstruct body)
ds.top("a"; n=2) # ["apple", "banana"] — kwargs supportedAsync progress with __status__
Indexed properties spawn background Tasks, and progress is wired into them automatically: __status__ defaults to a Treebars.initialize_progress!(:state; description="") root, and the default __substatus__ hangs a child node under it per property compute. Declare __status__ only to label the root (an empty description makes it a structural node that renders nothing and hoists its children), or set it to nothing to switch progress off.
Like any x = y in a struct body, that declaration is an overrideable default: a constructor kwarg seeds the cache and wins. @include kid = Child() therefore mounts the child under the parent's tree whatever Child declares; pass @include kid = Child(; __status__ = nothing) to silence that subtree instead.
@dynamicstruct struct MyApp
__status__ = initialize_progress!(:state; description="MyApp") # optional: labels the root
results(key) = expensive_computation(__status__) # __status__ is the substatus
end
app = MyApp()
# Non-blocking access with progress:
fetchindex(app.results, key) do rv, status
rv isa Pending ? render_progress(status) : render_result(rv)
end__substatus__ is called before each compute begins. name is the property symbol, args/kwargs are the indices. The returned object is stored in ThreadsafeDict.status (accessible via getstatus) and passed to the computation body as the local __status__.
__substatus__ fires on every generated-property compute — indexed (memoize!) and bare scalar (_bare_substatus_f) alike. It is skipped for dunder properties (__hash__, __status__, …) and for fixed struct fields, which have no body.
An undocumented property gets description="", i.e. a structural node that renders nothing and hoists its children; add a docstring to make it a labelled row in the tree.
In-struct property markers
These are not real macros — they are pattern-matched by @dynamicstruct inside a struct body. Outside a struct body they're either no-ops, real macros (e.g. @memo), or undefined. Don't rely on them in arbitrary positions.
| Marker | Effect |
|---|---|
@cached prop = expr | Persist to disk under cache_path. Per-key for indexed properties. |
@cached v"N" prop = expr | Versioned disk cache; bumping N invalidates files without changing inputs. |
@persist prop = expr | Write the in-memory value back to disk on demand (see @persist). |
@lru N prop(idx) = expr | Bound the per-property in-memory dict to N entries (LRU eviction). |
@memo prop = expr | Inside a struct: rewrite call → bracket access. Outside: process-wide function memoize. |
Cache inspection
Real macros — usable inside and outside @dynamicstruct bodies. Inside a body, drop the object prefix and use the bare property name.
DynamicObjects.@cache_status Macro
@cache_status o.prop
@cache_status o.prop(indices...)Return the disk-cache status of a @cached property as a Symbol:
:unstarted— no cache file exists yet.:started— an empty placeholder file exists (previous run may have crashed).:ready— a complete cache file exists and can be deserialized.
Can be used both outside and inside a @dynamicstruct body. Inside a struct definition, omit the object prefix — just use the property name (with parens for indexed properties).
# Outside the struct:
@cache_status e.result # :unstarted (before first access)
e.result
@cache_status e.result # :ready
@cache_status e.ci(2) # for indexed properties — call syntax
# Inside the struct body:
@dynamicstruct struct App
@cached result(key) = expensive(key)
status(key) = @cache_status result(key) # :unstarted, :started, or :ready
endThe legacy bracket form (@cache_status o.prop[indices...]) still works for backward compatibility but is discouraged in new code — prefer call syntax, which mirrors the way the property is invoked.
DynamicObjects.@is_cached Macro
@is_cached o.prop
@is_cached o.prop(indices...)Return true if the disk cache for o.prop (or o.prop(indices...)) is :ready, i.e. the cached value can be loaded from disk without recomputation.
Can be used both outside and inside a @dynamicstruct body. Inside a struct definition, omit the object prefix — just use the property name (with parens for indexed properties).
# Outside the struct:
@is_cached e.result # false before first access, true afterwards
# Inside the struct body:
@dynamicstruct struct App
@cached result(key) = expensive(key)
summary(key) = if @is_cached result(key)
"cached: $(@memo! result(key))"
else
"not yet computed"
end
endThe legacy bracket form (@is_cached o.prop[indices...]) still works for backward compatibility but is discouraged in new code.
DynamicObjects.@cache_path Macro
@cache_path o.prop
@cache_path o.prop(indices...)Return the file path where the disk-cached value of o.prop (or o.prop(indices...)) is (or would be) stored.
@cache_path e.result # e.g. "cache/<hash>/result.sjl"
@cache_path e.ci(2) # "cache/<hash>/ci_2.sjl"The legacy bracket form is still accepted but discouraged.
sourceDynamicObjects.@clear_cache! Macro
@clear_cache! o.prop
@clear_cache! o.prop(indices...)Clear the disk cache (and in-memory cache) for a @cached property.
Without indices, clears all cached entries for the property (both the in-memory value and all .sjl files for that property on disk). With indices, clears only the specific entry.
@clear_cache! e.result # clear all cached entries for `result`
@clear_cache! e.ci(3) # clear only the (3,) entryThe legacy bracket form is still accepted but discouraged.
sourceDynamicObjects.@persist Macro
@persist o.prop
@persist o.prop(indices...)Write the in-memory value of o.prop (or the indexed entry o.prop(indices...)) back to its disk cache. Use after mutating a value in place when the property was declared with @cached and the on-disk copy is now stale relative to the in-memory copy.
The legacy bracket form (@persist o.prop[indices...]) still works but is discouraged in new code — prefer call syntax.
Functions
DynamicObjects.remake Function
remake(obj; kwargs...)Create a new instance of the same @dynamicstruct type as obj, copying all fixed fields from obj and overriding any specified via keyword arguments.
Keyword arguments that correspond to fixed fields replace those field values in the new instance. Any remaining keyword arguments are forwarded to the constructor as cache pre-population overrides.
Because a @dynamicstruct is a pure function of its fixed fields, any already memoized property of obj whose (transitive) fixed-field dependencies are all unchanged is carried over to the new instance instead of being recomputed — the per-type carry set is baked from the dependson graph at macro-expansion, so the decision costs nothing at runtime. Impure properties (reading rand, the clock, or external mutable state) violate this contract and must not be relied on across remake.
Example
@dynamicstruct struct Config
n::Int
scale::Float64
base = sum(1:n) # depends only on n
result = scale * base # depends on scale (and, transitively, n)
end
c = Config(100, 2.0); c.result # memoizes base + result
c2 = remake(c; scale=3.0) # n unchanged → `base` CARRIED over; `result` recomputed
c3 = remake(c; n=200) # n changed → both base & result recomputed
c4 = remake(c; result=0.0) # result pre-set to 0.0 (explicit override wins)DynamicObjects.fetchindex Function
fetchindex(fetch, ip, indices...; kwargs...)Call memoize!(ip, indices...; kwargs...) with a custom fetch function.
For IndexableProperty backed by a ThreadsafeDict, the fetch callback receives (rv, status) where rv is a Pending handle (still computing) or the computed result (done), and status is the substatus object (from __substatus__) or nothing. fetch(::Pending) blocks for the value (rethrowing if the compute failed).
Pass force=true to unconditionally recompute: clears both the in-memory cache entry and the on-disk cache file so the next access recomputes from scratch.
Example
fetchindex(app.results, key) do rv, status
if rv isa Pending
# still computing — status is the progress node
render_progress(status)
else
# done — render result
render(rv)
end
endDynamicObjects.fetchindex! Function
fetchindex!(callback, ip, indices...; fetch=Base.fetch, kwargs...)In-place variant of fetchindex. When callback is nothing, falls through to a plain memoize!(ip, indices...; fetch, kwargs...) — useful for sites that opt out of the two-phase fetch dance without changing call shape.
DynamicObjects.getstatus Function
getstatus(ip::IndexableProperty, indices...; kwargs...)Return the status object associated with an in-flight computation for the given key, or nothing if no status exists (computation not started, already finished, or no __substatus__ defined).
Only meaningful for IndexableProperty backed by a ThreadsafeDict.
Cache maintenance
DynamicObjects.entries Function
entries(ip::IndexableProperty)Return a vector of (; key, state, status, value) for all entries in a ThreadsafeDict-backed IndexableProperty. state is one of :running, :failed, or :done. value is the cached result (for :done), a Pending handle (for :running), or the captured exception (for :failed). status is the substatus object or nothing.
DynamicObjects.cached_entries Function
cached_entries(ip::IndexableProperty)Return a vector of (key, value) pairs for completed (non-Task) entries only.
DynamicObjects.clear_all_caches! Function
clear_all_caches!(obj)Clear all @cached properties on a @dynamicstruct instance — both in-memory and on disk. Equivalent to clear_mem_caches! + clear_disk_caches!.
DynamicObjects.clear_mem_caches! Function
clear_mem_caches!(obj)Clear all in-memory memoized property values on a @dynamicstruct instance, leaving disk caches (@cached files) untouched. Every derived property — including child DOs stored as values — will be recomputed on next access.
This is useful after hot-reloading code via Revise: property values computed by old method definitions stay memoized until the process restarts or this function is called.
sourceDynamicObjects.clear_disk_caches! Function
clear_disk_caches!(obj)Delete all on-disk cache files for @cached properties on a @dynamicstruct instance. In-memory values are left intact (they'll be stale until clear_mem_caches! is also called, or until the process restarts).
Cancellation
Missing docstring.
Missing docstring for cancel!. Check Documenter's build log for details.
Missing docstring.
Missing docstring for cancel_all!. Check Documenter's build log for details.
Error handling
DynamicObjects.PropertyComputationError Type
PropertyComputationError <: ExceptionWraps an error that occurred during lazy property computation, adding context about which property failed (property name, type, indices/kwargs). The original exception and backtrace are stored in the cause field.
DynamicObjects.unwrap_error Function
unwrap_error(e)Recursively unwrap TaskFailedException, CompositeException, and PropertyComputationError wrappers to find the root cause exception.
Persistent / bounded collections
DynamicObjects.PersistentSet Type
PersistentSet(path)A thread-safe Set that persists to disk via Serialization. Loads existing data from path on construction, or starts empty if the file doesn't exist.
DynamicObjects.LazyPersistentDict Type
LazyPersistentDict{D<:AbstractDict}(path[, empty_data]; seed!)Threadsafe dict backed by Serialization.serialize/deserialize. The backing file path is resolved lazily via a callable path so the constructor itself is precompile-safe (no mkpath, no file I/O). The on-disk file is loaded on the first operation (double-checked under the lock), and the optional seed!(data) callback runs once after load if the dict is empty. Mutations persist to disk synchronously under the lock.
path may be an AbstractString (fixed path) or a 0-arg function returning a String. Pass an ordered backing dict (e.g. OrderedDict{K,V}()) to preserve insertion order.
Missing docstring.
Missing docstring for LRUDict. Check Documenter's build log for details.
Missing docstring.
Missing docstring for ThreadsafeLRUDict. Check Documenter's build log for details.
Pluggable key tracking
For bounding on-disk caches when the full key set isn't known up front.
DynamicObjects.KeyTracker Type
KeyTrackerAbstract type for pluggable accessed-keys persistence strategies. Implement record!(tracker, key) and load_keys(tracker) for custom strategies.
Override key_tracker(o, ::Val{name}) on your object type to select a strategy.
DynamicObjects.SharedFileTracker Type
SharedFileTracker(path)Default strategy: all pods/processes share a single _keys.sjl file. Simple, but not safe for concurrent multi-process writes to NFS.
Missing docstring.
Missing docstring for PerPodFileTracker. Check Documenter's build log for details.
DynamicObjects.NoKeyTracker Type
NoKeyTracker()No-op strategy: never records or loads keys. Use when tracking is unwanted.
sourceDynamicObjects.key_tracker Function
key_tracker(o, ::Val{name}) -> KeyTrackerReturn the KeyTracker to use for property name on object o. Override this method on your type to change the tracking strategy.
# Example: disable accessed-key tracking for all properties on MyType
DynamicObjects.key_tracker(o::MyType, ::Val{name}) where {name} =
DynamicObjects.NoKeyTracker()DynamicObjects.record! Function
record!(tracker::KeyTracker, key)Record that key was accessed, persisting according to the tracker's strategy.
DynamicObjects.load_keys Function
load_keys(tracker::KeyTracker) -> SetLoad the full set of recorded keys according to the tracker's strategy.
source