Skip to content

API Reference

Everything exported by DynamicObjects. For usage and worked examples see the manual.

The struct macro

DynamicObjects.@dynamicstruct Macro
julia
@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)
end

Define 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

julia
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)
julia
# 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)
julia
# 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 supported

Async 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.

julia
@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.

source

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.

MarkerEffect
@cached prop = exprPersist to disk under cache_path. Per-key for indexed properties.
@cached v"N" prop = exprVersioned disk cache; bumping N invalidates files without changing inputs.
@persist prop = exprWrite the in-memory value back to disk on demand (see @persist).
@lru N prop(idx) = exprBound the per-property in-memory dict to N entries (LRU eviction).
@memo prop = exprInside 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
julia
@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).

julia
# 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
end

The 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.

source
DynamicObjects.@is_cached Macro
julia
@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).

julia
# 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
end

The legacy bracket form (@is_cached o.prop[indices...]) still works for backward compatibility but is discouraged in new code.

source
DynamicObjects.@cache_path Macro
julia
@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.

julia
@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.

source
DynamicObjects.@clear_cache! Macro
julia
@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.

julia
@clear_cache! e.result        # clear all cached entries for `result`
@clear_cache! e.ci(3)         # clear only the (3,) entry

The legacy bracket form is still accepted but discouraged.

source
DynamicObjects.@persist Macro
julia
@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.

source

Functions

DynamicObjects.remake Function
julia
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

julia
@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)
source
DynamicObjects.fetchindex Function
julia
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

julia
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
end
source
DynamicObjects.fetchindex! Function
julia
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.

source
DynamicObjects.getstatus Function
julia
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.

source

Cache maintenance

DynamicObjects.entries Function
julia
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.

source
DynamicObjects.cached_entries Function
julia
cached_entries(ip::IndexableProperty)

Return a vector of (key, value) pairs for completed (non-Task) entries only.

source
DynamicObjects.clear_all_caches! Function
julia
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!.

source
DynamicObjects.clear_mem_caches! Function
julia
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.

source
DynamicObjects.clear_disk_caches! Function
julia
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).

source

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
julia
PropertyComputationError <: Exception

Wraps 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.

source
DynamicObjects.unwrap_error Function
julia
unwrap_error(e)

Recursively unwrap TaskFailedException, CompositeException, and PropertyComputationError wrappers to find the root cause exception.

source

Persistent / bounded collections

DynamicObjects.PersistentSet Type
julia
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.

source
DynamicObjects.LazyPersistentDict Type
julia
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.

source

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
julia
KeyTracker

Abstract 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.

source
DynamicObjects.SharedFileTracker Type
julia
SharedFileTracker(path)

Default strategy: all pods/processes share a single _keys.sjl file. Simple, but not safe for concurrent multi-process writes to NFS.

source

Missing docstring.

Missing docstring for PerPodFileTracker. Check Documenter's build log for details.

DynamicObjects.NoKeyTracker Type
julia
NoKeyTracker()

No-op strategy: never records or loads keys. Use when tracking is unwanted.

source
DynamicObjects.key_tracker Function
julia
key_tracker(o, ::Val{name}) -> KeyTracker

Return the KeyTracker to use for property name on object o. Override this method on your type to change the tracking strategy.

julia
# Example: disable accessed-key tracking for all properties on MyType
DynamicObjects.key_tracker(o::MyType, ::Val{name}) where {name} =
    DynamicObjects.NoKeyTracker()
source
DynamicObjects.record! Function
julia
record!(tracker::KeyTracker, key)

Record that key was accessed, persisting according to the tracker's strategy.

source
DynamicObjects.load_keys Function
julia
load_keys(tracker::KeyTracker) -> Set

Load the full set of recorded keys according to the tracker's strategy.

source
You are viewing the dev branch. This branch may include code written with Claude Code with less human supervision. Only human-approved code is merged into main.