# configuration

> Configuration worker for the iii engine.

| field | value |
|-------|-------|
| version | 0.16.1 |
| type | engine |
| repo | https://github.com/iii-hq/iii |
| author | iii |

## installation

```sh
iii worker add configuration@0.16.1
```

## configuration

```yaml
- adapter:
    config:
      directory: ./data/configuration
    name: fs
  ttl_seconds: 0
```

## readme

# configuration

Schema-validated, reactive registry of named configuration entries. Workers register their configuration ids with a JSON Schema, publish values that are validated against it, and emit `configuration` triggers on every change so other workers react without polling.

The default `fs` adapter stores one YAML file per id under a configurable directory and watches it for external edits — manual edits surface as `configuration:updated` events the same way SDK calls do. The `bridge` adapter delegates to a remote III Engine and re-broadcasts its events into the local fan-out. Reads expand `${VAR:default}` placeholders against the live process env on every call, so env changes propagate without a worker restart.

## Sample Configuration

```yaml
- name: configuration
  config:
    adapter:
      name: fs
      config:
        directory: ./data/configuration
    ttl_seconds: 0
```

## Configuration

| Field | Type | Description |
|---|---|---|
| `adapter` | Adapter | Adapter for configuration persistence. Defaults to `fs`. |
| `ttl_seconds` | integer | Per-id cleanup countdown in seconds. When `>0`, an entry whose last subscriber trigger has been unregistered for this long is deleted. Defaults to `0` (no cleanup). |

## Adapters

### fs

File-system adapter that stores one YAML file per configuration id and watches the directory for external edits.

```yaml
name: fs
config:
  directory: ./data/configuration
```

| Field | Type | Description |
|---|---|---|
| `directory` | string | Directory holding `<id>.yaml` files. Created on boot. Defaults to `./data/configuration`. |

External writes / edits / removals to the watched directory are debounced (500 ms) and replayed as `configuration:registered`, `configuration:updated`, or `configuration:deleted` events through the same trigger fan-out used by SDK calls.

### bridge

Forwards every `configuration::*` call to a remote III Engine via the iii-sdk and registers a remote `configuration` trigger so changes on the source engine are mirrored into the local trigger fan-out.

```yaml
name: bridge
config:
  bridge_url: ${REMOTE_III_URL:ws://localhost:49134}
```

| Field | Type | Description |
|---|---|---|
| `bridge_url` | string | WebSocket URL of the remote III Engine. Defaults to `ws://localhost:49134`. |

The `bridge` adapter cannot delete configurations on the remote engine; cleanup happens via the remote's TTL or by operating on the remote engine directly.

## Functions

### `configuration::register`

Declare a configuration id with a JSON Schema, name, description, and optional initial value. Idempotent — re-registering replaces the schema, name, description, and metadata; the stored value is kept unless `initial_value` is supplied. Validates `initial_value` against `schema` before persisting. Fires `configuration:registered` on first registration or `configuration:updated` on subsequent calls.

Parameters: `id` (string), `name` (string), `description` (string), `schema` (object — JSON Schema), `initial_value` (any, optional), `metadata` (any, optional)

Returns: the stored entry `{ id, name, description, schema, value, metadata }`. Templates inside `value` are stored verbatim; expansion happens on read.

### `configuration::set`

Replace the stored value for a registered id. Validates `value` against the registered schema. Fires `configuration:updated`.

Parameters: `id` (string), `value` (any)

Returns: `old_value` (any, or `null` if the entry had no prior value), `new_value` (any)

### `configuration::get`

Read one configuration by id.

Parameters: `id` (string), `raw` (boolean, optional — defaults to `false`)

Returns: `id` (string), `value` (any). When `raw` is `false`, `${VAR:default}` placeholders inside string fields are expanded against the live process env. When `raw` is `true`, the stored value is returned verbatim.

### `configuration::list`

Enumerate every registered configuration. Never returns the stored value — pair with `configuration::get` once you have the id you want.

Returns: `configurations` (array of `{ id, name, description, schema, metadata }`), sorted lexicographically by `id`.

### `configuration::schema`

Read the schema, name, description, and metadata for one id without exposing the value.

Parameters: `id` (string)

Returns: `id` (string), `name` (string), `description` (string), `schema` (object), `metadata` (any, optional)

### Error Codes

| Code | Meaning |
|---|---|
| `NOT_REGISTERED` | `set` was called against an id that has not been passed to `register` yet. |
| `INVALID_ID` | The id does not match `[a-z0-9_-]{1,64}` — the constraint applied so ids are safe filenames for the `fs` adapter. |
| `SCHEMA_INVALID` | The supplied value does not satisfy the registered JSON Schema. The error message lists each violation. |
| `NOT_FOUND` | `get` or `schema` was called against an id that is not registered. |
| `ADAPTER_ERROR` | The adapter failed to persist the change (disk error, bridge unreachable, etc.). |

## Trigger Type: `configuration`

Fires when a configuration entry is registered, updated, or deleted — including external `fs` file edits and bridge-forwarded events from a remote engine.

| Config Field | Type | Description |
|---|---|---|
| `configuration_id` | string | Only fire for changes to this id. When omitted, fires for every id. |
| `event_types` | string[] | Subset of `configuration:registered`, `configuration:updated`, `configuration:deleted`. When omitted, fires for every event type. |
| `condition_function_id` | string | Function ID for conditional execution. If it returns `false`, the handler is skipped. |

### Configuration Event Payload

| Field | Type | Description |
|---|---|---|
| `type` | string | Always `"configuration"`. |
| `event_type` | string | `"configuration:registered"`, `"configuration:updated"`, or `"configuration:deleted"`. |
| `id` | string | The configuration id that changed. |
| `name` | string | The registered name at the time of the event. |
| `description` | string | The registered description. |
| `schema` | object | The registered JSON Schema. |
| `old_value` | any | Previous value with `${VAR:default}` placeholders expanded. `null` on `configuration:registered`. |
| `new_value` | any | New value with `${VAR:default}` placeholders expanded. `null` on `configuration:deleted`. |
| `metadata` | any | Echoes the registered metadata; omitted when none was supplied. |

### Sample Code

```typescript
const fn = iii.registerFunction(
  { id: 'stream::onConfigChange' },
  async (event) => {
    console.log('Configuration changed:', event.event_type, event.id)
    console.log('New value:', event.new_value)
    return {}
  },
)

iii.registerTrigger({
  type: 'configuration',
  function_id: fn.id,
  config: {
    configuration_id: 'iii-stream',
    event_types: ['configuration:updated'],
  },
})
```

Mutations that fire triggers: `configuration::register` (`:registered` on first call, `:updated` on re-registration), `configuration::set` (`:updated`), TTL-driven cleanup (`:deleted`), and external `fs` create / edit / delete events. Reads (`configuration::get`, `configuration::list`, `configuration::schema`) do **not** fire triggers.

## TTL Cleanup

The worker tracks the number of triggers currently bound to each configuration id. When `ttl_seconds > 0` is configured AND the **last** trigger for an id is unregistered, a countdown deletes the entry after the TTL elapses. A new trigger registration before the countdown fires aborts the cleanup; while at least one trigger is bound to an id, that entry never expires.

`ttl_seconds = 0` (the default) disables expiry entirely — entries persist until they are explicitly deleted via the adapter or, for the `fs` adapter, by removing the underlying file.

This is the cleanup story for ephemeral workers that come and go (sandboxes, short-lived consumers) and should not leave stale configuration behind when no one is left subscribing.

## Usage Example: Migrating a Worker Off `config.yaml`

```typescript
import { registerWorker } from 'iii-sdk'

const iii = registerWorker('ws://localhost:49134')

// 1. Declare the schema once at startup.
await iii.trigger({
  function_id: 'configuration::register',
  payload: {
    id: 'iii-stream',
    name: 'Stream worker',
    description: 'Connection settings for the stream worker.',
    schema: {
      type: 'object',
      required: ['port'],
      properties: {
        port: { type: 'integer', minimum: 1, maximum: 65535 },
        host: { type: 'string' },
      },
    },
    initial_value: { port: 3112, host: '127.0.0.1' },
  },
})

// 2. Read the live value (placeholders expanded) anywhere you need it.
const { value } = await iii.trigger({
  function_id: 'configuration::get',
  payload: { id: 'iii-stream' },
})

// 3. Subscribe to changes elsewhere — set/external-edit/bridge events all
//    arrive on the same trigger.
const onChange = iii.registerFunction('stream::onConfigChange', async (event) => {
  console.log('Stream config now:', event.new_value)
  return {}
})

iii.registerTrigger({
  type: 'configuration',
  function_id: onChange.id,
  config: { configuration_id: 'iii-stream' },
})
```

## api reference

```json
{
  "functions": [
    {
      "description": "Read a configuration by id. Expands ${VAR:default} placeholders against the live process env unless raw=true is passed.",
      "metadata": {},
      "name": "configuration::get",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "id": {
            "type": "string"
          },
          "raw": {
            "default": false,
            "description": "When `true`, return the stored value verbatim (including `${VAR}` placeholders). When `false` (default), expand `${VAR:default}` against the live process env.",
            "type": "boolean"
          }
        },
        "required": [
          "id"
        ],
        "title": "ConfigurationGetInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "id": {
            "type": "string"
          },
          "value": true
        },
        "required": [
          "id",
          "value"
        ],
        "title": "ConfigurationGetResult",
        "type": "object"
      }
    },
    {
      "description": "List every registered configuration with id, name, description, and schema. Sorted by id; never returns the stored value.",
      "metadata": {},
      "name": "configuration::list",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "ConfigurationListInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "ConfigurationSchemaView": {
            "description": "Subset of a [`ConfigurationEntry`] returned from `list` / `schema` — the schema-only view that never leaks the stored value.",
            "properties": {
              "description": {
                "type": "string"
              },
              "id": {
                "type": "string"
              },
              "metadata": true,
              "name": {
                "type": "string"
              },
              "schema": true
            },
            "required": [
              "description",
              "id",
              "name",
              "schema"
            ],
            "type": "object"
          }
        },
        "properties": {
          "configurations": {
            "items": {
              "$ref": "#/definitions/ConfigurationSchemaView"
            },
            "type": "array"
          }
        },
        "required": [
          "configurations"
        ],
        "title": "ConfigurationListResult",
        "type": "object"
      }
    },
    {
      "description": "Register a configuration id with a name, description, and JSON Schema. Idempotent — re-registering replaces metadata and (when initial_value is provided) the value. Validates initial_value against the schema.",
      "metadata": {},
      "name": "configuration::register",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "description": {
            "description": "Description shown in `configuration::list`.",
            "type": "string"
          },
          "id": {
            "description": "Configuration id. Must match `[a-z0-9_-]{1,64}` so it is safe as a filename in the `fs` adapter.",
            "type": "string"
          },
          "initial_value": {
            "description": "Optional initial value to install. Validated against `schema`."
          },
          "metadata": {
            "description": "Optional opaque metadata stored alongside the entry."
          },
          "name": {
            "description": "Human-readable name shown in `configuration::list`.",
            "type": "string"
          },
          "schema": {
            "description": "JSON Schema describing the value shape. `set` validates against this."
          }
        },
        "required": [
          "description",
          "id",
          "name",
          "schema"
        ],
        "title": "ConfigurationRegisterInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "A single configuration entry tracked by the worker.\n\n`value` is stored verbatim — including any `${VAR:default}` template strings. Expansion happens on read (`configuration::get`) and during trigger fan-out so env-var changes propagate without a worker restart.",
        "properties": {
          "description": {
            "description": "One-line description of what this configuration controls.",
            "type": "string"
          },
          "id": {
            "description": "Stable identifier (e.g. `iii-stream`, `iii-observability`).",
            "type": "string"
          },
          "metadata": {
            "description": "Optional caller-supplied metadata (owner team, change ticket, etc.)."
          },
          "name": {
            "description": "Human-readable display name.",
            "type": "string"
          },
          "schema": {
            "description": "JSON Schema describing the shape of `value`."
          },
          "value": {
            "default": null,
            "description": "Stored configuration body. May contain `${VAR:default}` placeholders."
          }
        },
        "required": [
          "description",
          "id",
          "name",
          "schema"
        ],
        "title": "ConfigurationEntry",
        "type": "object"
      }
    },
    {
      "description": "Retrieve the schema, name, and description for a configuration id. Mirrors a single entry from configuration::list.",
      "metadata": {},
      "name": "configuration::schema",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "id": {
            "type": "string"
          }
        },
        "required": [
          "id"
        ],
        "title": "ConfigurationSchemaInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Subset of a [`ConfigurationEntry`] returned from `list` / `schema` — the schema-only view that never leaks the stored value.",
        "properties": {
          "description": {
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "metadata": true,
          "name": {
            "type": "string"
          },
          "schema": true
        },
        "required": [
          "description",
          "id",
          "name",
          "schema"
        ],
        "title": "ConfigurationSchemaView",
        "type": "object"
      }
    },
    {
      "description": "Replace the value of an already-registered configuration. Validates the value against the registered JSON Schema and emits a configuration:updated event.",
      "metadata": {},
      "name": "configuration::set",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "id": {
            "type": "string"
          },
          "value": {
            "description": "New configuration value. Validated against the registered schema."
          }
        },
        "required": [
          "id",
          "value"
        ],
        "title": "ConfigurationSetInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "new_value": {
            "description": "Current stored value (templates not expanded)."
          },
          "old_value": {
            "description": "Previous stored value, or `null` when the entry had no value yet."
          }
        },
        "required": [
          "new_value"
        ],
        "title": "ConfigurationSetResult",
        "type": "object"
      }
    }
  ],
  "triggers": []
}
```
