Module lifecycle management with dependency resolution.
Provides runtime lifecycle management for modules with explicit dependency declaration and automatic startup ordering.
This namespace provides a lightweight registry for managing module lifecycle:
(ns my.module
(:require [patcho.lifecycle :as lifecycle]))
(lifecycle/register-module! :my/module
{:depends-on [:other/module]
:setup (fn []
(create-tables!))
:cleanup (fn []
(drop-tables!))
:start (fn []
(start-connections!)
(subscribe-to-events!))
:stop (fn []
(unsubscribe!)
(close-connections!))})
(ns my.app
(:require
my.database ; Registers :my/database
my.cache ; Registers :my/cache (depends on :my/database)
my.api ; Registers :my/api (depends on :my/cache)
[patcho.lifecycle :as lifecycle]))
;; Set default store once (typically in main or init)
(lifecycle/set-store! (lifecycle/->FileLifecycleStore ".lifecycle"))
;; Simple approach: Just start! (auto-runs setup if needed)
(lifecycle/start! :my/api)
;; → Setups (if needed) and starts: :my/database → :my/cache → :my/api
;; Setup is idempotent - only runs once, tracked via lifecycle store
;; OR explicit control: Run setup separately (optional)
(lifecycle/setup! :my/api) ; One-time: setup all dependencies
(lifecycle/start! :my/api) ; Runtime: start all dependencies
;; Visualize dependencies
(lifecycle/print-dependency-tree :my/api)
;; :my/api
;; └── :my/cache
;; └── :my/database
;; Later: stop (RECURSIVE - stops dependents first)
(lifecycle/stop! :my/database)
;; → Stops: :my/api → :my/cache → :my/database
;; Surgical stop (only this module)
(lifecycle/stop-only! :my/api)
;; Cleanup (RECURSIVE with reference checking)
(lifecycle/cleanup! :my/api)
;; → Cleans :my/api
;; → Cleans :my/cache (if no other modules depend on it)
;; → Cleans :my/database (if no other modules depend on it)
Lifecycle management is complementary to version management:
They work together:
Note: start! automatically runs setup for modules that have a setup function, making the system easy to use while maintaining explicit control when needed.
Module lifecycle management with dependency resolution.
Provides runtime lifecycle management for modules with explicit dependency
declaration and automatic startup ordering.
## Design Philosophy
This namespace provides a lightweight registry for managing module lifecycle:
- Modules register themselves at namespace load time
- Dependencies are declared explicitly
- Operations are RECURSIVE - automatically handle dependencies
- Cleanup uses reference counting (claimed-by semantics)
- State tracking prevents double-start/stop
- Setup phase is separate from start/stop (one-time vs runtime)
## Usage
### Registration (at namespace load time)
(ns my.module
(:require [patcho.lifecycle :as lifecycle]))
(lifecycle/register-module! :my/module
{:depends-on [:other/module]
:setup (fn []
(create-tables!))
:cleanup (fn []
(drop-tables!))
:start (fn []
(start-connections!)
(subscribe-to-events!))
:stop (fn []
(unsubscribe!)
(close-connections!))})
### Application Startup
(ns my.app
(:require
my.database ; Registers :my/database
my.cache ; Registers :my/cache (depends on :my/database)
my.api ; Registers :my/api (depends on :my/cache)
[patcho.lifecycle :as lifecycle]))
;; Set default store once (typically in main or init)
(lifecycle/set-store! (lifecycle/->FileLifecycleStore ".lifecycle"))
;; Simple approach: Just start! (auto-runs setup if needed)
(lifecycle/start! :my/api)
;; → Setups (if needed) and starts: :my/database → :my/cache → :my/api
;; Setup is idempotent - only runs once, tracked via lifecycle store
;; OR explicit control: Run setup separately (optional)
(lifecycle/setup! :my/api) ; One-time: setup all dependencies
(lifecycle/start! :my/api) ; Runtime: start all dependencies
;; Visualize dependencies
(lifecycle/print-dependency-tree :my/api)
;; :my/api
;; └── :my/cache
;; └── :my/database
;; Later: stop (RECURSIVE - stops dependents first)
(lifecycle/stop! :my/database)
;; → Stops: :my/api → :my/cache → :my/database
;; Surgical stop (only this module)
(lifecycle/stop-only! :my/api)
;; Cleanup (RECURSIVE with reference checking)
(lifecycle/cleanup! :my/api)
;; → Cleans :my/api
;; → Cleans :my/cache (if no other modules depend on it)
;; → Cleans :my/database (if no other modules depend on it)
## Relationship to patcho.patch
Lifecycle management is complementary to version management:
- patcho.patch: Version migrations (data/schema state transitions)
- patcho.lifecycle: Runtime management (start/stop with dependencies)
They work together:
1. Apply patches to reach target version (patch/level!)
2. Start runtime services (lifecycle/start!) - auto-runs setup if needed
Note: start! automatically runs setup for modules that have a setup function,
making the system easy to use while maintaining explicit control when needed.
## Design Decisions
- Registry pattern: Modules stored in atom (like Patcho's patches)
- Keyword topics: Same style as Patcho (:my/module)
- Explicit dependencies: Clear, validated at runtime
- RECURSIVE operations: start/setup/cleanup handle deps automatically
- Auto-setup: start! runs setup if needed (idempotent via lifecycle store)
- Reference counting: cleanup only removes if not claimed by others
- State tracking: Prevents double-start bugs
- Functions not protocols: Simple, direct
- Minimal dependencies: Just clojure.coreDynamic var for the lifecycle store.
Defaults to AtomLifecycleStore (in-memory). State is lost on restart unless you explicitly set a persistent store (FileLifecycleStore, database, etc.).
Set globally via set-store!, or temporarily via with-store macro. State is automatically migrated when switching stores.
Dynamic var for the lifecycle store. Defaults to AtomLifecycleStore (in-memory). State is lost on restart unless you explicitly set a persistent store (FileLifecycleStore, database, etc.). Set globally via set-store!, or temporarily via with-store macro. State is automatically migrated when switching stores.
(cleanup! topic)Run one-time cleanup for a module and all its dependants (modules that depend on it).
Cleanup order:
This ensures no module is left in an inconsistent state with a missing dependency. Dependencies (modules this one depends on) are NOT cleaned - they may be shared.
This is idempotent - tracks if cleanup already ran via LifecycleStore.
Uses lifecycle-store - set it via set-store! or with-store macro.
Parameters: topic - Module keyword
Example: (set-store! (->FileLifecycleStore ".lifecycle")) (cleanup! :my/database) ; Also cleans up :my/cache if it depends on :my/database
Run one-time cleanup for a module and all its dependants (modules that depend on it). Cleanup order: 1. First cleanup all dependants (modules that depend on this one) - recursively 2. Then cleanup this module This ensures no module is left in an inconsistent state with a missing dependency. Dependencies (modules this one depends on) are NOT cleaned - they may be shared. This is idempotent - tracks if cleanup already ran via LifecycleStore. Uses *lifecycle-store* - set it via set-store! or with-store macro. Parameters: topic - Module keyword Example: (set-store! (->FileLifecycleStore ".lifecycle")) (cleanup! :my/database) ; Also cleans up :my/cache if it depends on :my/database
(clear-errors!)Clear all recorded module errors.
Useful after fixing issues and before retrying startup.
Example: (clear-errors!) (start! :my/module) ; Fresh start without old errors
Clear all recorded module errors. Useful after fixing issues and before retrying startup. Example: (clear-errors!) (start! :my/module) ; Fresh start without old errors
(dependency-graph)Returns the dependency graph as a map.
Useful for visualization or debugging.
Returns: {topic {:depends-on [...] :dependents [...]}}
Example: (dependency-graph) ;; => {:my/database {:depends-on [] :dependents [:my/cache]} ;; :my/cache {:depends-on [:my/database] :dependents [:my/api]} ;; :my/api {:depends-on [:my/cache] :dependents []}}
Returns the dependency graph as a map.
Useful for visualization or debugging.
Returns:
{topic {:depends-on [...] :dependents [...]}}
Example:
(dependency-graph)
;; => {:my/database {:depends-on [] :dependents [:my/cache]}
;; :my/cache {:depends-on [:my/database] :dependents [:my/api]}
;; :my/api {:depends-on [:my/cache] :dependents []}}(dependency-tree-string)(dependency-tree-string topic)Generate ASCII tree visualization of module dependencies.
With no arguments, shows all root modules (modules nothing depends on). With topic argument, shows dependency tree for that module.
Parameters: topic - (Optional) Module keyword to visualize
Returns: String with ASCII tree visualization
Example: (dependency-tree-string :my/api) ;; => ":my/api ;; ├── :my/cache ;; │ └── :my/database ;; └── :my/auth ;; └── :my/database"
Generate ASCII tree visualization of module dependencies. With no arguments, shows all root modules (modules nothing depends on). With topic argument, shows dependency tree for that module. Parameters: topic - (Optional) Module keyword to visualize Returns: String with ASCII tree visualization Example: (dependency-tree-string :my/api) ;; => ":my/api ;; ├── :my/cache ;; │ └── :my/database ;; └── :my/auth ;; └── :my/database"
Persistent storage for lifecycle state tracking.
Tracks one-time operations (setup, cleanup) across process restarts. Runtime state (started?) is NOT persisted - it's in-memory only.
Persistent storage for lifecycle state tracking. Tracks one-time operations (setup, cleanup) across process restarts. Runtime state (started?) is NOT persisted - it's in-memory only.
(read-lifecycle-state store topic)Read persistent state for a topic. Returns map with :setup-complete?, :cleanup-complete?, etc.
Read persistent state for a topic. Returns map with :setup-complete?, :cleanup-complete?, etc.
(write-lifecycle-state store topic state)Write persistent state for a topic. State is a map with :setup-complete?, :cleanup-complete?, etc.
Write persistent state for a topic. State is a map with :setup-complete?, :cleanup-complete?, etc.
(migrate-store! from-store to-store topics)Migrate lifecycle state from one store to another.
Useful for bootstrapping: start with AtomLifecycleStore, setup database, then migrate state to database-backed store.
Args: from-store - Source LifecycleStore to read from to-store - Destination LifecycleStore to write to topics - Collection of topic keywords to migrate (or nil for all registered)
Returns: Set of migrated topics
Example: ;; Bootstrap with in-memory store (def bootstrap-store (->AtomLifecycleStore (atom {}))) (set-store! bootstrap-store) (setup! :synthigy/database)
;; Migrate to database store (migrate-store! bootstrap-store db/db nil) (set-store! db/db)
Migrate lifecycle state from one store to another.
Useful for bootstrapping: start with AtomLifecycleStore, setup database,
then migrate state to database-backed store.
Args:
from-store - Source LifecycleStore to read from
to-store - Destination LifecycleStore to write to
topics - Collection of topic keywords to migrate (or nil for all registered)
Returns:
Set of migrated topics
Example:
;; Bootstrap with in-memory store
(def bootstrap-store (->AtomLifecycleStore (atom {})))
(set-store! bootstrap-store)
(setup! :synthigy/database)
;; Migrate to database store
(migrate-store! bootstrap-store db/*db* nil)
(set-store! db/*db*)Registry of module errors. Structure: {topic {:error Exception :timestamp Date}}
Registry of module errors. Structure: {topic {:error Exception :timestamp Date}}
(module-info topic)Returns info map for a module, or nil if not registered.
If lifecycle-store is set, also includes :setup-complete? and :cleanup-complete?.
Returns: {:depends-on [...] :started? true/false :has-setup? true/false :has-cleanup? true/false :setup-complete? true/false ; Only if lifecycle-store is set :cleanup-complete? true/false} ; Only if lifecycle-store is set
Returns info map for a module, or nil if not registered.
If *lifecycle-store* is set, also includes :setup-complete? and :cleanup-complete?.
Returns:
{:depends-on [...]
:started? true/false
:has-setup? true/false
:has-cleanup? true/false
:setup-complete? true/false ; Only if *lifecycle-store* is set
:cleanup-complete? true/false} ; Only if *lifecycle-store* is set(print-dependency-tree)(print-dependency-tree topic)Print ASCII tree visualization of module dependencies.
With no arguments, shows all root modules. With topic argument, shows dependency tree for that module.
Parameters: topic - (Optional) Module keyword to visualize
Example: (print-dependency-tree :my/api) ;; Prints: ;; :my/api ;; ├── :my/cache ;; │ └── :my/database ;; └── :my/auth ;; └── :my/database
Print ASCII tree visualization of module dependencies. With no arguments, shows all root modules. With topic argument, shows dependency tree for that module. Parameters: topic - (Optional) Module keyword to visualize Example: (print-dependency-tree :my/api) ;; Prints: ;; :my/api ;; ├── :my/cache ;; │ └── :my/database ;; └── :my/auth ;; └── :my/database
(print-system-report)Print formatted system status report showing started/stopped modules and errors.
Uses markers: [OK] - Started modules [ ] - Stopped modules (ready to start) [!] - Modules with errors or missing dependencies
Example: (print-system-report) ;; === System Report === ;; Registered: 7 | Started: 3 | Stopped: 4 ;; ;; [OK] Started modules: ;; * :synthigy/transit ;; * :synthigy/database -> [:synthigy/transit] ;; ;; [ ] Stopped modules (ready to start): ;; * :synthigy/iam -> [:synthigy/dataset] ;; ;; [!] Modules with errors: ;; * :synthigy/admin -> [:synthigy/dataset] ;; ERROR at 2026-01-07 18:23:45 ;; Message: No such var: db/db ;; Data: {:module :synthigy/admin}
Print formatted system status report showing started/stopped modules and errors.
Uses markers:
[OK] - Started modules
[ ] - Stopped modules (ready to start)
[!] - Modules with errors or missing dependencies
Example:
(print-system-report)
;; === System Report ===
;; Registered: 7 | Started: 3 | Stopped: 4
;;
;; [OK] Started modules:
;; * :synthigy/transit
;; * :synthigy/database -> [:synthigy/transit]
;;
;; [ ] Stopped modules (ready to start):
;; * :synthigy/iam -> [:synthigy/dataset]
;;
;; [!] Modules with errors:
;; * :synthigy/admin -> [:synthigy/dataset]
;; ERROR at 2026-01-07 18:23:45
;; Message: No such var: db/*db*
;; Data: {:module :synthigy/admin}(print-topology-layers)Print layered list visualization of all modules.
Shows modules grouped by dependency depth with explicit dependencies.
Example: (print-topology-layers) ;; Prints: ;; Layer 0 (foundation): ;; transit ;; ;; Layer 1: ;; database → [transit] ;; ;; Layer 2: ;; dataset → [database]
Print layered list visualization of all modules. Shows modules grouped by dependency depth with explicit dependencies. Example: (print-topology-layers) ;; Prints: ;; Layer 0 (foundation): ;; transit ;; ;; Layer 1: ;; database → [transit] ;; ;; Layer 2: ;; dataset → [database]
(register-module! topic
{:keys [depends-on setup cleanup start stop]
:or {stop (fn [] nil) start (fn [] nil)}})Register a module with its lifecycle functions and dependencies.
This should be called at namespace load time (top-level form).
Parameters: topic - Keyword identifying the module (e.g., :synthigy/iam) spec - Map with keys: :depends-on - Vector of topic keywords this module depends on :setup - (Optional) fn taking no args for one-time setup :cleanup - (Optional) fn taking no args for one-time cleanup :start - fn taking no args to start runtime services :stop - fn taking no args to stop runtime services
Example: (register-module! :my/database {:start (fn [] (connect! db-config)) :stop (fn [] (disconnect!))})
(register-module! :my/cache {:depends-on [:my/database] :setup (fn [] (create-cache-tables!)) :cleanup (fn [] (drop-cache-tables!)) :start (fn [] (start-cache!)) :stop (fn [] (stop-cache!))})
Register a module with its lifecycle functions and dependencies.
This should be called at namespace load time (top-level form).
Parameters:
topic - Keyword identifying the module (e.g., :synthigy/iam)
spec - Map with keys:
:depends-on - Vector of topic keywords this module depends on
:setup - (Optional) fn taking no args for one-time setup
:cleanup - (Optional) fn taking no args for one-time cleanup
:start - fn taking no args to start runtime services
:stop - fn taking no args to stop runtime services
Example:
(register-module! :my/database
{:start (fn [] (connect! db-config))
:stop (fn [] (disconnect!))})
(register-module! :my/cache
{:depends-on [:my/database]
:setup (fn [] (create-cache-tables!))
:cleanup (fn [] (drop-cache-tables!))
:start (fn [] (start-cache!))
:stop (fn [] (stop-cache!))})(registered-modules)Returns vector of all registered module topics.
Returns vector of all registered module topics.
(reset-registry!)Clear all registered modules.
WARNING: This is primarily for testing. Using in production may leave modules in inconsistent state.
Stop your running modules first before calling this.
Clear all registered modules. WARNING: This is primarily for testing. Using in production may leave modules in inconsistent state. Stop your running modules first before calling this.
(reset-store!)Reset lifecycle store to a fresh in-memory AtomLifecycleStore.
Use this when cleaning up the database to clear all persisted state and start fresh. Does NOT migrate state (old state is discarded).
Example: ;; In database cleanup (reset-store!) ; Fresh atom store, no state
Reset lifecycle store to a fresh in-memory AtomLifecycleStore. Use this when cleaning up the database to clear all persisted state and start fresh. Does NOT migrate state (old state is discarded). Example: ;; In database cleanup (reset-store!) ; Fresh atom store, no state
(restart! topic)Restart a module (stop-only then start).
Uses stop-only! (non-recursive) for surgical control. Useful for REPL development when code changes.
Parameters: topic - Module keyword
Example: (restart! :my/cache)
Restart a module (stop-only then start). Uses stop-only! (non-recursive) for surgical control. Useful for REPL development when code changes. Parameters: topic - Module keyword Example: (restart! :my/cache)
(set-store! store)Set the default lifecycle store globally.
Automatically migrates state from the previous store to the new one. This makes store transitions seamless (e.g., from bootstrap AtomLifecycleStore to database-backed store).
Arguments: store - Implementation of LifecycleStore protocol (or nil to clear)
Example: ;; Bootstrap with in-memory store (set-store! (->AtomLifecycleStore (atom {}))) (setup! :my/database)
;; Switch to DB - state auto-migrates (set-store! db/db)
(setup! :my/app) ; Uses database store
Set the default lifecycle store globally.
Automatically migrates state from the previous store to the new one.
This makes store transitions seamless (e.g., from bootstrap AtomLifecycleStore
to database-backed store).
Arguments:
store - Implementation of LifecycleStore protocol (or nil to clear)
Example:
;; Bootstrap with in-memory store
(set-store! (->AtomLifecycleStore (atom {})))
(setup! :my/database)
;; Switch to DB - state auto-migrates
(set-store! db/*db*)
(setup! :my/app) ; Uses database store(setup! & topics)Run one-time setup for one or more modules RECURSIVELY.
This is idempotent - tracks if setup already ran via LifecycleStore. STARTS dependencies first (setup often needs them running).
Uses lifecycle-store - set it via set-store! or with-store macro.
Parameters: topics - One or more module keywords
Example: (set-store! (->FileLifecycleStore ".lifecycle")) (setup! :my/cache) (setup! :my/db :my/cache :my/api)
Run one-time setup for one or more modules RECURSIVELY. This is idempotent - tracks if setup already ran via LifecycleStore. STARTS dependencies first (setup often needs them running). Uses *lifecycle-store* - set it via set-store! or with-store macro. Parameters: topics - One or more module keywords Example: (set-store! (->FileLifecycleStore ".lifecycle")) (setup! :my/cache) (setup! :my/db :my/cache :my/api)
(setup-complete? topic)(start! topic)(start! topic & more-topics)Start a module RECURSIVELY (starts all dependencies first).
This is idempotent - won't start if already started. Dependencies are started in correct order automatically. Records errors in module-errors atom if startup fails.
If module has a setup function and setup hasn't been completed yet, automatically runs setup first (idempotent via lifecycle store).
Parameters: topic - Module keyword
Example: (start! :my/api) ;; → Setups (if needed) and starts: :my/database → :my/cache → :my/api
Start a module RECURSIVELY (starts all dependencies first). This is idempotent - won't start if already started. Dependencies are started in correct order automatically. Records errors in module-errors atom if startup fails. If module has a setup function and setup hasn't been completed yet, automatically runs setup first (idempotent via lifecycle store). Parameters: topic - Module keyword Example: (start! :my/api) ;; → Setups (if needed) and starts: :my/database → :my/cache → :my/api
(started-modules)Returns vector of currently started module topics.
Returns vector of currently started module topics.
(started? topic)Returns true if module is currently started.
Returns true if module is currently started.
(stop! topic)(stop! topic & more-topics)Stop a module and all its dependents RECURSIVELY.
Stop order (dependents first):
This ensures no module is left running without its dependencies. This is symmetric with start! (which starts dependencies first).
This is idempotent - won't stop if already stopped.
Parameters: topic - Module keyword
Example: (stop! :my/database) ;; → Stops: :my/api → :my/cache → :my/database (dependents first)
Stop a module and all its dependents RECURSIVELY. Stop order (dependents first): 1. First stop all dependents (modules that depend on this one) - recursively 2. Then stop this module This ensures no module is left running without its dependencies. This is symmetric with start! (which starts dependencies first). This is idempotent - won't stop if already stopped. Parameters: topic - Module keyword Example: (stop! :my/database) ;; → Stops: :my/api → :my/cache → :my/database (dependents first)
(stop-only! topic)Stop a single module (NOT recursive - only stops this module).
This is idempotent - won't stop if already stopped. Use this for surgical control when you don't want to affect dependents.
Parameters: topic - Module keyword
Example: (stop-only! :my/api) ; Only stops :my/api, dependencies stay running
Stop a single module (NOT recursive - only stops this module). This is idempotent - won't stop if already stopped. Use this for surgical control when you don't want to affect dependents. Parameters: topic - Module keyword Example: (stop-only! :my/api) ; Only stops :my/api, dependencies stay running
(system-report)Get comprehensive system status report including errors.
Returns map with: :registered - Total count of registered modules :started - Count of started modules :stopped - Count of stopped modules :modules - Map of {topic {:status :started/:stopped :depends-on [...] :dependents [...] :missing-dependencies [...] :error {:message "..." :timestamp Date :exception Exception}}}
Example: (system-report) ;=> {:registered 7 :started 3 :stopped 4 :modules {:synthigy/transit {:status :started ...} :synthigy/admin {:status :stopped :error {:message "No such var: db/db" :timestamp #inst "2026-01-07" :exception #<ExceptionInfo ...>}}}}}
Get comprehensive system status report including errors.
Returns map with:
:registered - Total count of registered modules
:started - Count of started modules
:stopped - Count of stopped modules
:modules - Map of {topic {:status :started/:stopped
:depends-on [...]
:dependents [...]
:missing-dependencies [...]
:error {:message "..."
:timestamp Date
:exception Exception}}}
Example:
(system-report)
;=> {:registered 7
:started 3
:stopped 4
:modules {:synthigy/transit {:status :started ...}
:synthigy/admin {:status :stopped
:error {:message "No such var: db/*db*"
:timestamp #inst "2026-01-07"
:exception #<ExceptionInfo ...>}}}}}(topology-layers-string)Generate layered list visualization of all modules.
Shows modules grouped by dependency depth with explicit dependencies.
Returns: String with layered list visualization
Example: (topology-layers-string) ;; => "Layer 0 (foundation): ;; :synthigy/transit ;; ;; Layer 1: ;; :synthigy/database → [:synthigy/transit] ;; ;; Layer 2: ;; :synthigy/dataset → [:synthigy/database] ;; ;; Layer 3: ;; :synthigy/iam → [:synthigy/dataset] ;; :synthigy/lacinia → [:synthigy/dataset]"
Generate layered list visualization of all modules. Shows modules grouped by dependency depth with explicit dependencies. Returns: String with layered list visualization Example: (topology-layers-string) ;; => "Layer 0 (foundation): ;; :synthigy/transit ;; ;; Layer 1: ;; :synthigy/database → [:synthigy/transit] ;; ;; Layer 2: ;; :synthigy/dataset → [:synthigy/database] ;; ;; Layer 3: ;; :synthigy/iam → [:synthigy/dataset] ;; :synthigy/lacinia → [:synthigy/dataset]"
(with-store store & body)Execute body with a specific LifecycleStore bound to lifecycle-store.
Useful for testing or temporary overrides.
Arguments: store - Implementation of LifecycleStore protocol body - Expressions to execute with the store bound
Example: (with-store (->FileLifecycleStore ".test-lifecycle") (setup! :my/app) (cleanup! :other-app))
Execute body with a specific LifecycleStore bound to *lifecycle-store*.
Useful for testing or temporary overrides.
Arguments:
store - Implementation of LifecycleStore protocol
body - Expressions to execute with the store bound
Example:
(with-store (->FileLifecycleStore ".test-lifecycle")
(setup! :my/app)
(cleanup! :other-app))cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |