tiramisu/dev/extension

Build typed components and renderer-scoped systems for Tiramisu.

Extension authors work with concrete Gleam data throughout the public API:

  1. Define a Schema(data) from a valid default and typed fields.
  2. Create an entity Kind(data) or scene-root kind.
  3. Expose Value helpers for application views.
  4. Build a component with entity-local state and lifecycle callbacks.
  5. Optionally build systems that coordinate that kind within one renderer.
  6. Return both from a Plugin and register it with tiramisu.register_with.

Values are declarative. Components and systems receive decoded data and return Command values for runtime-managed objects, events, and async work. They never receive raw DOM attribute dictionaries or mutable renderer state.

A complete component

This component rotates its entity root before each rendered frame:

import gleam/time/duration
import gleam/time/timestamp
import savoiardi/object
import tiramisu/dev/extension

pub type Spin {
  Spin(speed: Float)
}

type Definitions {
  Definitions(
    kind: extension.Kind(Spin),
    speed: extension.Property(Spin, Float),
  )
}

fn definitions() -> Definitions {
  let #(speed_field, speed_property) =
    extension.field(
      name: "speed",
      codec: extension.float_codec(),
      get: fn(data: Spin) { data.speed },
      set: fn(_data: Spin, speed: Float) { Spin(speed:) },
    )

  Definitions(
    kind: extension.kind(
      "spin",
      extension.schema(Spin(speed: 1.0), [speed_field]),
    ),
    speed: speed_property,
  )
}

pub fn speed(value: Float) -> extension.Value(msg) {
  let definitions = definitions()
  extension.property(definitions.kind, definitions.speed, value)
}

fn tick(
  context: extension.Context,
  state: Nil,
  data: Spin,
  delta: duration.Duration,
  _now: timestamp.Timestamp,
) -> #(Nil, List(extension.Command)) {
  let entity = extension.entity(context)
  let _ =
    object.rotate_y(
      extension.root(entity),
      data.speed *. duration.to_seconds(delta),
    )
  #(state, [])
}

fn component() -> extension.Component {
  extension.component(kind: definitions().kind, mount: fn(_context, _data) {
    #(Nil, [])
  })
  |> extension.on_tick(extension.BeforeRender, tick)
  |> extension.build_component
}

pub fn plugin() -> extension.Plugin {
  extension.plugin(name: "spin", components: [component()], systems: [])
}

An application registers this plugin once with tiramisu.register_with([spin.plugin()]), then adds spin.speed(2.0) to any tiramisu.entity, primitive, mesh, camera, light, or empty.

Components and systems

A component instance belongs to one (entity, kind, optional instance key) identity. Its state, event listeners, tasks, object slots, and cleanup are isolated from every other instance. Use a component for local behavior.

A system is instantiated once per renderer and subscribes to typed component mount, update, and unmount notifications. Use a system for renderer-wide indexes, physics, arbitration, or batched behavior.

Both can opt into BeforeRender and AfterRender. Within either phase, systems run first, followed by components in scene and component order.

Ownership

Lifecycle callbacks return commands rather than mutating runtime storage. The runtime associates each command with its originating component, named instance, entity incarnation, system, and renderer generation. This gives object slots one owner and prevents late events or async settlements from targeting a replacement entity or reconnected renderer.

See docs/extensions.md in the Tiramisu repository for a larger guide.

Types

@superdocs Schema and Values - Core Types Encodes and decodes one schema field type.

Prefer the built-in codec constructors for common values. Use codec for a domain-specific JSON representation.

pub opaque type Codec(value)

@superdocs Entities and Commands - Command Types A runtime-managed effect returned by component and system callbacks.

Commands retain their origin, entity incarnation, and renderer generation. This is how Tiramisu enforces ownership and rejects stale work.

pub opaque type Command

@superdocs Plugins - Core Types The validated component and system registry used by renderer runtimes.

This value is produced internally during registration. Application and extension code normally works with Plugin instead.

pub opaque type CompiledPlugins

@superdocs Components - Core Types A validated, type-erased component definition ready for a plugin.

Construct this only with build_component. The erased callbacks retain their concrete schema and state types without exposing dynamic casts.

pub opaque type Component

@superdocs Components - Core Types A typed component definition under construction.

data is decoded from the component schema. state is private to each mounted entity and optional instance-key identity. Add lifecycle hooks, then erase the concrete types with build_component.

pub opaque type ComponentBuilder(data, state)

@superdocs Components - Core Types Runtime-owned state for one mounted component identity.

Extension authors do not construct or inspect this value; lifecycle callbacks work with the concrete state chosen by component.

pub opaque type ComponentInstance

@superdocs Entities and Commands - Context Types Capabilities passed to one component lifecycle callback.

Use entity to reach the owning entity and component_instance to distinguish named instances. Context does not expose runtime storage.

pub opaque type Context

@superdocs Entities and Commands - Context Types An opaque handle to one entity incarnation in one renderer.

An entity id may be reused after unmount, so the runtime also carries an internal incarnation. Commands and events made with an old handle cannot affect a replacement entity with the same id.

pub opaque type Entity

@superdocs Schema and Values - Core Types One named field in a component schema.

Construct fields with field. The type parameter is the complete component data type, not the field value type.

pub opaque type Field(data)

@superdocs Schema and Values - Core Types Distinguishes multiple instances of the same component kind on one entity.

Instance keys are local to a component kind and entity. Use instance_key to construct one and named to apply it to a value.

pub opaque type InstanceKey

@superdocs Schema and Values - Core Types Identifies one typed component definition and its entity or scene scope.

The same kind value ties together view helpers, the component builder, and system subscriptions without dynamic casts.

pub opaque type Kind(data)

@superdocs Entities and Commands - Command Types Describes a change to one named object slot.

Object-change callbacks run before a replaced or removed object is disposed. This lets dependent components stop using the previous object safely.

pub type ObjectChange {
  ObjectSet(slot: String, object: object.Object3D)
  ObjectReplaced(
    slot: String,
    previous: object.Object3D,
    current: object.Object3D,
  )
  ObjectRemoved(slot: String, previous: object.Object3D)
}

Constructors

@superdocs Plugins - Core Types A named collection of component and system definitions.

Register user plugins with tiramisu.register_with. Tiramisu’s built-in plugin is always compiled first, followed by user plugins in list order.

pub opaque type Plugin

@superdocs Schema and Values - Core Types A typed setter for one field of a component kind.

field returns a property alongside its schema field. Keep it with the corresponding Kind and use it to expose declarative view helpers through property.

pub opaque type Property(data, value)

@superdocs Plugins - Errors Explains why plugin compilation or Lustre registration failed.

Registration is atomic: a validation failure installs none of the proposed renderer runtime.

pub type RegisterError {
  DuplicatePlugin(name: String)
  DuplicateComponent(name: String)
  DuplicateSystem(name: String)
  DuplicateField(component: String, field: String)
  InvalidName(name: String)
  LustreRegistrationFailed(reason: lustre.Error)
}

Constructors

  • DuplicatePlugin(name: String)
  • DuplicateComponent(name: String)
  • DuplicateSystem(name: String)
  • DuplicateField(component: String, field: String)
  • InvalidName(name: String)
  • LustreRegistrationFailed(reason: lustre.Error)

@superdocs Schema and Values - Core Types Defines the complete data accepted by a component.

Missing fields retain their values from default. Invalid initial data mounts the default; invalid update data preserves the previous valid value.

pub opaque type Schema(data)

@superdocs Systems - Core Types A type-erased renderer-scoped system definition ready for a plugin.

Construct this only with build_system.

pub opaque type System

@superdocs Systems - Core Types A renderer-scoped system definition under construction.

state is instantiated independently for every renderer. Add typed subscriptions, tick phases, and cleanup before calling build_system.

pub opaque type SystemBuilder(state)

@superdocs Entities and Commands - Context Types Renderer-local capabilities passed to one system callback.

A system can access its Savoiardi scene and renderer and look up entities within that renderer. Contexts never cross renderer hosts.

pub opaque type SystemContext

@superdocs Systems - Core Types Runtime-owned state for one system instance in one renderer.

Extension authors work with the concrete state supplied to system; the runtime uses this erased value to keep heterogeneous systems in order.

pub opaque type SystemInstance

@superdocs Entities and Commands - Command Types Selects when an opted-in component or system tick runs.

BeforeRender systems and components run before the scene is drawn. AfterRender systems and components run afterwards. Systems run before components within either phase.

pub type TickPhase {
  BeforeRender
  AfterRender
}

Constructors

  • BeforeRender
  • AfterRender

@superdocs Schema and Values - Core Types A declarative component value or wrapped Lustre attribute.

Applications place values in the lists accepted by Tiramisu scene and entity constructors. Values for one component are folded in list order, so later properties win.

pub opaque type Value(msg)

Values

pub fn bool_codec() -> Codec(Bool)

@superdocs Schema and Values - Built-in Codecs Return the standard JSON boolean codec.

pub fn build_component(
  builder: ComponentBuilder(data, state),
) -> Component

@superdocs Components - Building Finish a component builder and erase its concrete data and state types.

The resulting component can be included in exactly one registered plugin list; duplicate component names are rejected during registration.

pub fn build_system(builder: SystemBuilder(state)) -> System

@superdocs Systems - Building Finish a system builder and erase its concrete state type.

Include the result in a plugin’s systems list.

pub fn cancel_task(key key: String) -> Command

@superdocs Entities and Commands - Async Work Invalidate the current same-origin task settlement for key.

This does not abort the underlying promise. Later success or failure is ignored; resource_task still disposes a rejected successful value.

pub fn codec(
  encode encode: fn(value) -> json.Json,
  decode decode: decode.Decoder(value),
) -> Codec(value)

@superdocs Schema and Values - Schema Construction Create a codec from a JSON encoder and Gleam dynamic decoder.

Decoding errors are handled at the component boundary and reported as tiramisu:component-error; they are not exposed to lifecycle callbacks.

pub fn component(
  kind kind: Kind(data),
  mount mount: fn(Context, data) -> #(state, List(Command)),
) -> ComponentBuilder(data, state)

@superdocs Components - Building Start a component builder with its required mount callback.

mount receives decoded schema data and returns the initial private state plus commands. The runtime stores that state before attaching listeners and executing mount commands.

pub fn component_instance(
  context: Context,
) -> Result(InstanceKey, Nil)

@superdocs Entities and Commands - Entity Capabilities Return the current component instance key.

The unnamed component returns Error(Nil); a named component returns its key in Ok.

pub fn emit(
  entity: Entity,
  name: String,
  detail: json.Json,
) -> Command

@superdocs Entities and Commands - Events Dispatch a bubbling DOM event from an entity with JSON detail.

Component event handlers registered through on_event receive the same event only while their entity incarnation and component identity are live.

pub fn entity(context: Context) -> Entity

@superdocs Entities and Commands - Entity Capabilities Return the entity that owns the current component instance.

pub fn entity_id(entity: Entity) -> String

@superdocs Entities and Commands - Entity Capabilities Return an entity’s DOM id.

The id is unique only within the current renderer. It does not encode the entity incarnation.

pub fn field(
  name name: String,
  codec codec: Codec(value),
  get get: fn(data) -> value,
  set set: fn(data, value) -> data,
) -> #(Field(data), Property(data, value))

@superdocs Schema and Values - Schema Construction Define one schema field and its matching typed property.

get reads the field from the complete data value. set returns a new complete data value with only that field changed.

pub fn find_entity(
  context: SystemContext,
  id: String,
) -> Result(Entity, Nil)

@superdocs Entities and Commands - System Capabilities Find a currently mounted entity by id within this system’s renderer.

Do not retain the returned handle after the entity unmounts. Commands made with a stale handle are ignored.

pub fn float_codec() -> Codec(Float)

@superdocs Schema and Values - Built-in Codecs Return the standard JSON floating-point codec.

pub fn html_attribute(
  attribute: attribute.Attribute(msg),
) -> Value(msg)

@superdocs Schema and Values - Declarative Values Include an ordinary Lustre attribute in a Tiramisu value list.

This is commonly used to expose typed component event handlers next to the component’s property helpers.

pub fn instance_key(value: String) -> InstanceKey

@superdocs Schema and Values - Named Instances Create a named component instance key.

Naming two values differently gives them independent lifecycle state, task keys, and system subscription identities.

pub fn instance_key_to_string(key: InstanceKey) -> String

@superdocs Schema and Values - Named Instances Return the string stored by an instance key.

pub fn int_codec() -> Codec(Int)

@superdocs Schema and Values - Built-in Codecs Return the standard JSON integer codec.

pub fn kind(
  name name: String,
  schema schema: Schema(data),
) -> Kind(data)

@superdocs Schema and Values - Schema Construction Create a component kind that can be mounted on ordinary entities.

Names use lowercase letters and digits separated by single hyphens.

pub fn list_codec(item: Codec(a)) -> Codec(List(a))

@superdocs Schema and Values - Built-in Codecs Build a JSON array codec from an item codec.

pub fn named(
  value value: Value(msg),
  instance instance: InstanceKey,
) -> Value(msg)

@superdocs Schema and Values - Named Instances Apply an instance key to a component value.

This leaves wrapped HTML attributes unchanged. The unnamed component and each named component are separate identities.

pub fn object(
  entity: Entity,
  slot: String,
) -> Result(object.Object3D, Nil)

@superdocs Entities and Commands - Entity Capabilities Find the object currently attached to a named entity slot.

Returns Error(Nil) when the slot is empty. This lookup does not transfer ownership of the object.

pub fn on_component_mount(
  builder: SystemBuilder(state),
  kind: Kind(data),
  handle: fn(
    SystemContext,
    state,
    Entity,
    Result(InstanceKey, Nil),
    data,
  ) -> #(state, List(Command)),
) -> SystemBuilder(state)

@superdocs Systems - Subscriptions Subscribe to successful mounts of one typed component kind.

The callback receives renderer-local context, current system state, the owning entity, optional instance identity, and decoded component data. instance is Error(Nil) for the unnamed component.

pub fn on_component_unmount(
  builder: SystemBuilder(state),
  kind: Kind(data),
  handle: fn(
    SystemContext,
    state,
    Entity,
    Result(InstanceKey, Nil),
    data,
  ) -> #(state, List(Command)),
) -> SystemBuilder(state)

@superdocs Systems - Subscriptions Subscribe to unmounts of one typed component kind.

Use this to remove entity and instance identities from renderer-wide indexes before those entities or components are disposed.

pub fn on_component_update(
  builder: SystemBuilder(state),
  kind: Kind(data),
  handle: fn(
    SystemContext,
    state,
    Entity,
    Result(InstanceKey, Nil),
    data,
    data,
  ) -> #(state, List(Command)),
) -> SystemBuilder(state)

@superdocs Systems - Subscriptions Subscribe to valid updates of one typed component kind.

The callback receives previous and current decoded data. Updates from every entity and named instance in this renderer feed the same system state.

pub fn on_event(
  builder: ComponentBuilder(data, state),
  name: String,
  decoder: decode.Decoder(event),
  handle: fn(Context, state, data, event) -> #(
    state,
    List(Command),
  ),
) -> ComponentBuilder(data, state)

@superdocs Components - Events and Objects Subscribe this component instance to a named DOM event.

Event detail is parsed with decoder. Valid events update component state and produce commands; invalid detail is ignored. The listener is scoped to the entity incarnation and removed before unmount.

pub fn on_object_change(
  builder: ComponentBuilder(data, state),
  slot: String,
  handle: fn(Context, state, data, ObjectChange) -> #(
    state,
    List(Command),
  ),
) -> ComponentBuilder(data, state)

@superdocs Components - Events and Objects Observe changes to one named object slot on the same entity.

This is useful when a component depends on an object produced by another component. The callback runs before a replaced or removed object is disposed.

pub fn on_system_stop(
  builder: SystemBuilder(state),
  stop: fn(SystemContext, state) -> List(Command),
) -> SystemBuilder(state)

@superdocs Systems - Lifecycle Add guaranteed cleanup for one renderer-scoped system.

The callback runs during renderer disconnect after entities have unmounted. Pending tasks from this system are invalidated before cleanup commands run.

pub fn on_system_tick(
  builder: SystemBuilder(state),
  phase: TickPhase,
  tick: fn(
    SystemContext,
    state,
    duration.Duration,
    timestamp.Timestamp,
  ) -> #(state, List(Command)),
) -> SystemBuilder(state)

@superdocs Systems - Lifecycle Opt this system into one frame phase.

Systems tick before components in each phase and retain registration order. Adding another callback for the same phase replaces the previous callback.

pub fn on_tick(
  builder: ComponentBuilder(data, state),
  phase: TickPhase,
  tick: fn(
    Context,
    state,
    data,
    duration.Duration,
    timestamp.Timestamp,
  ) -> #(state, List(Command)),
) -> ComponentBuilder(data, state)

@superdocs Components - Lifecycle Opt this component into one frame phase.

The callback receives the current state, current component data, elapsed duration, and frame timestamp. Adding another callback for the same phase replaces the previous callback.

pub fn on_unmount(
  builder: ComponentBuilder(data, state),
  unmount: fn(Context, state, data) -> List(Command),
) -> ComponentBuilder(data, state)

@superdocs Components - Lifecycle Add guaranteed teardown for one mounted component identity.

The runtime invalidates pending tasks and detaches listeners before this callback. It executes returned teardown commands before disposing object slots and resources owned by the component.

pub fn on_update(
  builder: ComponentBuilder(data, state),
  update: fn(Context, state, data, data) -> #(
    state,
    List(Command),
  ),
) -> ComponentBuilder(data, state)

@superdocs Components - Lifecycle Handle a valid change to this component’s decoded data.

The callback receives previous and current data. It does not run when the normalized encoded value is unchanged. Invalid updates preserve the previous data and state without invoking this callback.

pub fn optional_codec(item: Codec(a)) -> Codec(option.Option(a))

@superdocs Schema and Values - Built-in Codecs Build a nullable JSON codec from an item codec.

pub fn plugin(
  name name: String,
  components components: List(Component),
  systems systems: List(System),
) -> Plugin

@superdocs Plugins - Assembly Assemble one named extension plugin.

Names use lowercase letters and digits separated by single hyphens. Plugin, component, and system names must be globally unique across built-ins and all user plugins registered in the same call.

pub fn property(
  kind kind: Kind(data),
  property property: Property(data, value),
  value property_value: value,
) -> Value(msg)

@superdocs Schema and Values - Declarative Values Create a declarative update for one typed property.

Tiramisu starts from the schema default or the preceding value for this kind in the same attribute list, sets the property, and serializes the result.

pub fn remove_object(entity: Entity, slot: String) -> Command

@superdocs Entities and Commands - Object Slots Remove and dispose a named object slot owned by the command origin.

Removing an empty slot is a no-op. A component or system cannot remove a slot owned by another origin.

pub fn resource_task(
  key key: String,
  start start: fn() -> promise.Promise(Result(value, error)),
  success success: fn(value) -> List(Command),
  failure failure: fn(error) -> List(Command),
  dispose_stale dispose_stale: fn(value) -> Nil,
) -> Command

@superdocs Entities and Commands - Async Work Start async work whose successful value needs stale-result cleanup.

This has the same ownership and key semantics as task. If a successful value settles after its task became stale, dispose_stale runs exactly once and success commands are not applied. An accepted value must transfer to one owner, commonly through set_object.

pub fn root(entity: Entity) -> object.Object3D

@superdocs Entities and Commands - Entity Capabilities Return the stable hierarchy root for this entity incarnation.

Transform this object directly. Attach replaceable models, geometry, or other component-owned objects through set_object slots.

pub fn scene_kind(
  name name: String,
  schema schema: Schema(data),
) -> Kind(data)

@superdocs Schema and Values - Schema Construction Create a component kind that can be mounted only on the scene root.

Tiramisu rejects an entity-scoped kind on the scene, or a scene-scoped kind on an entity, with an invalid_scope component error.

pub fn schema(
  default default: data,
  fields fields: List(Field(data)),
) -> Schema(data)

@superdocs Schema and Values - Schema Construction Create a schema from a complete valid default and its fields.

Field names must be unique. Registration returns DuplicateField when a component schema contains the same name more than once.

pub fn set_object(
  entity: Entity,
  slot: String,
  object: object.Object3D,
) -> Command

@superdocs Entities and Commands - Object Slots Attach or replace an object in a named entity slot.

The command origin owns the slot. Replacing a slot owned by the same origin disposes the previous object after notifying subscribers. A different owner is rejected and the incoming object is disposed.

pub fn string_codec() -> Codec(String)

@superdocs Schema and Values - Built-in Codecs Return the standard JSON string codec.

pub fn system(
  name name: String,
  start start: fn(SystemContext) -> #(state, List(Command)),
) -> SystemBuilder(state)

@superdocs Systems - Building Start a renderer-scoped system builder.

start runs once for each connected renderer and returns that renderer’s initial system state plus commands. Systems start in plugin registration order before the first scene reconciliation.

pub fn system_active_camera(
  context: SystemContext,
) -> Result(camera.Camera, Nil)

@superdocs Entities and Commands - System Capabilities Return the active camera selected within this system’s renderer.

Returns Error(Nil) until a mounted camera component has claimed the renderer-local active-camera role.

pub fn system_renderer(
  context: SystemContext,
) -> renderer.Renderer

@superdocs Entities and Commands - System Capabilities Return the Savoiardi renderer owned by this system instance.

pub fn system_scene(context: SystemContext) -> scene.Scene

@superdocs Entities and Commands - System Capabilities Return the Savoiardi scene owned by this system’s renderer.

pub fn task(
  key key: String,
  start start: fn() -> promise.Promise(Result(value, error)),
  success success: fn(value) -> List(Command),
  failure failure: fn(error) -> List(Command),
) -> Command

@superdocs Entities and Commands - Async Work Start origin-local asynchronous work when this command is accepted.

start is lazy and runs only in a connected browser runtime. A newer task with the same caller-provided key supersedes an older task from the same component, named instance, or system. Other origins may use the same key independently.

success and failure turn settlement into commands. Removing the origin or reconnecting its renderer invalidates settlement. Cancellation does not promise to abort the underlying JavaScript promise.

pub fn value(kind: Kind(data), data: data) -> Value(msg)

@superdocs Schema and Values - Declarative Values Create a declarative value that replaces the complete data for a kind.

Use this when callers naturally construct the complete data record. For independently composable fields, expose helpers built with property.

pub fn vec2_float_codec() -> Codec(vec2.Vec2(Float))

@superdocs Schema and Values - Built-in Codecs Return a codec for a float vector encoded as { "x": Float, "y": Float }.

pub fn vec2_int_codec() -> Codec(vec2.Vec2(Int))

@superdocs Schema and Values - Built-in Codecs Return a codec for an integer vector encoded as { "x": Int, "y": Int }.

pub fn vec3_float_codec() -> Codec(vec3.Vec3(Float))

@superdocs Schema and Values - Built-in Codecs Return a codec for a float vector encoded as x, y, and z JSON fields.

Search Document