Claude-skill-registry fx-registry

Create new Sandestin effect registries following project conventions. Use when adding effects, actions, or placeholders. Keywords: registry, effect, action, placeholder, handler, create, new.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/fx-registry" ~/.claude/skills/majiayu000-claude-skill-registry-fx-registry && rm -rf "$T"
manifest: skills/data/fx-registry/SKILL.md
source content

Sandestin Registry Author

Create effect registries for Sandestin projects.

About Sandestin

Sandestin is a Clojure effect dispatch library with schema-driven discoverability. Registries define effects, actions, and placeholders that can be composed and dispatched.

GitHub: https://github.com/brianium/sandestin

Check if Installed

Look for the dependency in

deps.edn
:

io.github.brianium/sandestin {:git/tag "v0.3.0" :git/sha "2be6acc"}

Install if Missing

Add to

deps.edn
under
:deps
:

{:deps
 {io.github.brianium/sandestin {:git/tag "v0.3.0" :git/sha "2be6acc"}}}

Workflow

1. Check for Existing Patterns

# Find existing registries
find src -name "*.clj" | xargs grep -l "::s/effects" 2>/dev/null

# Check naming conventions
grep -r "defn registry" src/

2. Create the Registry

Simple Registry (no config)

(ns mylib.fx.logging
  "Logging effects."
  (:require [ascolais.sandestin :as s]))

(def registry
  {::s/effects
   {:mylib.log/info
    {::s/description "Log an info message"
     ::s/schema [:tuple [:= :mylib.log/info] :string]
     ::s/handler (fn [_ctx _system msg]
                   (println "[INFO]" msg))}}})

Configurable Registry (with dependencies)

(ns mylib.fx.database
  "Database effects."
  (:require [ascolais.sandestin :as s]))

(defn registry
  "Database effects registry.

   Requires a datasource."
  [datasource]
  {::s/effects
   {:mylib.db/query
    {::s/description "Execute a SQL query"
     ::s/schema [:tuple [:= :mylib.db/query] :string [:* :any]]
     ::s/system-keys [:datasource]
     ::s/handler (fn [_ctx system sql & params]
                   (jdbc/execute! (:datasource system) (into [sql] params)))}}

   ::s/system-schema
   {:datasource [:fn some?]}})

3. Registration Patterns

Effect (side-effecting):

{:<ns>/<verb>
 {::s/description "What this effect does"
  ::s/schema [:tuple [:= :<ns>/<verb>] <arg-schemas>]
  ::s/system-keys [:key1 :key2]
  ::s/handler (fn [{:keys [dispatch dispatch-data system]} system & args]
                ;; Do side effect, optionally dispatch continuation
                )}}

Continuation dispatch arities:

The

dispatch
function in handler context supports three arities:

ArityPurpose
(dispatch fx)
Dispatch with current system and dispatch-data
(dispatch extra-data fx)
Merge
extra-data
into dispatch-data
(dispatch system-override extra-data fx)
Merge into both system and dispatch-data
;; Add data for placeholders in continuation
(dispatch {:result result} continuation-fx)

;; Override system for nested effects (e.g., route to different connection)
(dispatch {:sse alt-connection}  ;; merged into system
          {:request-id id}       ;; merged into dispatch-data
          continuation-fx)

Action (pure, returns effect vectors):

{:<ns>/<action>
 {::s/description "What this action does"
  ::s/schema [:tuple [:= :<ns>/<action>] <arg-schema>]
  ::s/handler (fn [state & args]
                [[:<ns>/effect1 arg]
                 [:<ns>/effect2 (:val state)]])}}

;; If actions need state from system:
::s/system->state (fn [system] @(:app-state system))

Placeholder (resolves from dispatch-data):

{:<ns>/<placeholder>
 {::s/description "What value this provides"
  ::s/schema [:tuple [:= :<ns>/<placeholder>]]
  ::s/handler (fn [dispatch-data]
                (:some-key dispatch-data))}}

;; Placeholder with arguments
{:<ns>/<placeholder>
 {::s/description "What value this provides"
  ::s/schema [:tuple [:= :<ns>/<placeholder>] <arg-schema>]
  ::s/handler (fn [dispatch-data arg]
                (get-in dispatch-data [:results arg]))}}

Self-Preserving Placeholder (for async continuations):

When an effect dispatches continuation effects with new data, placeholders in those continuations must "self-preserve" — return themselves when the data isn't available yet, then resolve when re-interpolated with the actual data.

{:<ns>/<result>
 {::s/description "Result from async operation, self-preserving"
  ::s/schema [:tuple [:= :<ns>/<result>]]
  ::s/handler (fn [dispatch-data]
                ;; Return self if data not yet available
                (or (:<ns>/<result> dispatch-data)
                    [:<ns>/<result>]))}}

Usage pattern:

;; Effect that dispatches continuation with result
{:<ns>/fetch
 {::s/handler
  (fn [{:keys [dispatch]} system url continuation-fx]
    (let [result (http/get url)]
      ;; Dispatch continuation with result in dispatch-data
      (dispatch {:<ns>/result result} continuation-fx))
    :fetch-started)}}

;; Calling code - placeholder resolves in continuation dispatch
(dispatch {} {}
  [[:<ns>/fetch "http://api.example.com"
    [[:<ns>/process [:<ns>/result]]]]])

Flow:

  1. Initial dispatch interpolates
    [:<ns>/result]
    → returns itself (no data yet)
  2. Effect runs, calls
    dispatch
    with
    {:<ns>/result actual-data}
  3. Continuation dispatch interpolates
    [:<ns>/result]
    → returns
    actual-data

4. Wire Registries to Dispatch

Important:

create-dispatch
takes a vector of registries, not a single registry.

;; Correct - vector of registries
(def dispatch (s/create-dispatch [my-registry]))

;; Wrong - will fail
(def dispatch (s/create-dispatch my-registry))

Combining Multiple Registries

Registries can be simple maps or

[registry-fn & args]
vectors for configurable registries:

(ns myapp.dispatch
  (:require [ascolais.sandestin :as s]
            [myapp.fx.db :as db]
            [myapp.fx.logging :as logging]
            [myapp.fx.http :as http]))

(defn create-dispatch
  "Create dispatch with all application registries."
  [{:keys [datasource http-client]}]
  (s/create-dispatch
    [[db/registry datasource]      ;; configurable - passes datasource arg
     logging/registry              ;; simple - no config needed
     [http/registry http-client]])) ;; configurable - passes client arg

Registry Merge Behavior

When registries have overlapping keys:

  • Effects, actions, placeholders: Later registry wins (with warning via tap>)
  • Interceptors: Concatenated in order
  • system-schema: Merged (later wins per key)

5. Test via REPL

(require '[ascolais.sandestin :as s])
(require '[mylib.fx.logging :as logging])

;; Create a test dispatch
(def dispatch (s/create-dispatch [logging/registry]))

;; Verify registration
(s/describe dispatch :mylib.log/info)
(s/sample dispatch :mylib.log/info)

;; Test it
(dispatch {} {} [[:mylib.log/info "hello"]])

Required Fields

FieldPurpose
::s/description
Human-readable description
::s/schema
Malli schema for how the effect is called
::s/handler
Implementation function

Schema Format

Schemas describe invocation shape, NOT return types. The schema specifies how to call the effect — the effect key and its arguments:

[:tuple [:= :qualified/keyword] <arg-schemas...>]

Examples:

;; Effect taking a string argument
::s/schema [:tuple [:= :mylib.log/info] :string]
;; Called as: [:mylib.log/info "hello"]

;; Effect taking a set of symbols
::s/schema [:tuple [:= :mylib.stock/analyze] [:set :string]]
;; Called as: [:mylib.stock/analyze #{"AAPL" "GOOG"}]

;; Effect taking a map argument
::s/schema [:tuple [:= :mylib.report/render] [:map {:description "Report data"}]]
;; Called as: [:mylib.report/render {:title "Q4" :data [...]}]

Do NOT document return types in schemas. Schemas are for discoverability of how to invoke effects, not for validating outputs.

Optional Fields

FieldPurpose
::s/system-keys
Declare system map dependencies
::s/system-schema
Malli schemas for system keys
::s/system->state
Extract immutable state for actions

Placeholder Patterns

PatternWhen to Use
SimpleValue available at dispatch time (e.g., DOM event data)
Self-preservingValue available later via continuation dispatch (e.g., async results)
TransformingWraps another placeholder to transform its value

Transforming placeholder example:

;; Extract field from async result
{:<ns>/result-name
 {::s/description "Extract name from async result"
  ::s/schema [:tuple [:= :<ns>/result-name]]
  ::s/handler
  (fn [dispatch-data]
    ;; Check if result is available before transforming
    (if-let [result (:<ns>/result dispatch-data)]
      (:name result)
      [:<ns>/result-name]))}}

;; Usage: [:<ns>/result-name] instead of nesting