Skip to content

Cog: shared state model

Authored August 21, 2026.

Cog has separate Swift and Kotlin libraries. This page defines the runtime model and behavior they share. The Swift and Kotlin docs define platform-appropriate API spelling, physical representation, UI adapters, and native integration.

Cog owns state semantics on both platforms. Observation and Compose translate changed Cog values into native UI updates. A choice shared by both adapters still needs a place in this model before it becomes a shared rule. See design history only when you need background.

Why Cog exists

Cog manages an app as a graph of small state values. Some values are writable sources. Others are computed from them. This gives each fact one source of truth and lets the UI update only where a value changed.

Cog also solves two common problems:

  • Heavy setup can push features to create their own state systems. Then one fact may have several writable copies.
  • Separate streams can publish one at a time. A reader may then see a new input with an old computed value.

Cog keeps setup small and publishes each change as one complete state.

Core rules

  1. Keep it simple. State should be easy to declare, read, and change on each platform.
  2. Make every read correct. A normal read uses the last complete turn and updates every value it needs first. It never returns a mixed or stale state.
  3. Keep overhead low. Avoid extra user code, UI work, memory use, locks, and bookkeeping. Use measurements to choose internal designs.
  4. Keep one source of truth. One running app has one Cog graph. Each mutable fact has one writable source in that graph. Screens and features do not create their own copies.
  5. Keep one runtime model. Swift and Kotlin use the same graph behavior, turn ordering, lifetime rules, and async guarantees. Native frameworks connect at the UI and operating-system seams. Cog defines the rules behind those seams.

Speed must not weaken correctness or create more sources of truth. Internal speed work must not make normal app code harder to read.

Cog stores sources and cached automatic values. It records graph edges, stages turns, settles reads, applies equality, and owns async and lifetime state. A native UI adapter records the exact state a UI scope reads, then invalidates that scope when a completed turn changes the state. The application value stays in Cog; the adapter carries a change token.

Main parts

PartMeaning
DescriptorA stable name for state; it does not hold the app's live value
SourceThe one writable input for a mutable fact
Automatic stateA saved value computed from other state
Keyed boxOne state shape used for many keys, such as account IDs
RuntimeThe app-wide owner of values, graph links, turns, lifetimes, and async work
Read capabilityThe limited object used for tracked or untracked reads
WriterThe turn-only object used to stage source values
OperationA named app action that changes the graph
Reaction or effectWork that changes something outside the graph
UI boundaryNative UI tracking for the exact state a UI scope reads
RigThe unit an app grows by: one prefix's models, declarations, UI adapters, and effect owners

Platform names differ where needed:

Shared partSwiftKotlin
RuntimeCogsCogStore
Keyed stateCogBox and value referencesCogBox and descriptor-plus-key reads
Operationa CogOps methoda CogStore extension
Side-effect ownerassembly Mechanismlifecycle-owned CogEffects
Async stateCogStatus through the optional status lensCogPhase
UI boundaryObservation and the SwiftUI environmentCompose state tokens and a composition-local store
Rig<Rig>Rig+<Aspect>.swift filesnot yet specified

Descriptors name state; runtimes store it

A descriptor defines identity and behavior. It does not store a live app value. The app runtime stores that value and its graph links. Tests can use the same descriptor in a separate runtime without changing production state.

Rendering diagram…

Production has one runtime. Features may own descriptor files, but they do not own separate graphs. State that matters only to one view stays in the native view-state tool. Rebuilding a screen must not reset Cog state. A named operation resets it when the app calls for a reset.

Sources and automatic state

Use a source where data enters the graph, such as a user choice, service result, clock tick, or outside event. Most other state should be automatic: a read-only value computed from the state it reads.

Each run records its real reads as dependencies. A branch can stop reading one parent and start reading another. A keyed loop can add or remove parents. The runtime updates the graph links after the run.

An untracked read is available when a value should affect an action but should not trigger it again. Its API must stand out. Normal computed state and effects use tracked reads.

A dependency cycle is a programmer error. The error must show the descriptor and key path that formed the cycle.

Keyed state

A keyed box defines one state shape for many items. The descriptor and key together form the state identity. A change to one ZIP code, account, or row does not notify every item in the box.

Unused keyed values may expire based on platform lifetime and cache rules. Each platform chooses how keys are passed, hashed, and stored.

Reads and turns

A turn is one complete graph change. A normal read uses the last finished turn. Before it returns, the runtime updates, or settles, the needed path. It does not update every possible value after every source write.

A writer read during a turn sees source values already staged by that turn. This makes read-change-write work correct without showing partial state.

One outer turn call:

  1. stages source writes through a writer;
  2. drops writes equal to the current value;
  3. marks possible changes below those sources;
  4. settles live values that must be current;
  5. publishes changed values to UI and stream readers; and
  6. runs affected effects in a fixed order.

Unused branches stay lazy. If an automatic value stays equal, the change stops there. A reaction may ask for another turn, but it cannot change the finished turn it is reading.

Swift runs its graph on the MainActor. Kotlin runs the same runtime model on the store lane. Each implementation owns dependency capture, staging, settlement, equality, and publication. Storage layouts remain platform-specific.

Writes are named operations

App code calls a clear action such as selectLocation, acceptWeather, or increment. That operation starts the turn and owns access to its private sources. A button, effect, or service callback does not write directly to graph storage.

This makes writes easy to find and keeps one writer for each fact. Swift uses access control and CogLint. Kotlin uses private descriptors and writer-only extensions.

Put related source writes in one turn when readers must see them together. For example, publish a weather report and its warning flag in the same turn.

Async state

A turn does not wait for outside work. A tracked selector reads its inputs and describes the work. Starting, success, failure, replacement, and each stream item enter the graph in their own complete turns.

Both platforms must show uncertainty clearly:

  • pending, success, and failure are different states;
  • no accepted value is different from an accepted optional value;
  • an old result cannot publish after newer work replaces it; and
  • overlap follows a named rule such as latest, queue, or exhaust-latest.

Swift normally returns the last accepted value, or its declared default, and puts request details in CogStatus. Kotlin's first design returns CogPhase, which includes the previous value. The case names may differ; honest status and safe stale results may not.

Freshness and lifetime are separate. A saved value may be old but still kept. A fresh value may have no users and be ready to release. Work that must survive app shutdown belongs in platform storage and background tools, not a long-lived Cog task.

Effects stay outside computed state

Automatic state only computes state. It does not navigate, log, write files, send notices, or call hardware. A named reaction or effect owns that work and has a clear lifetime.

An effect may track graph state, make untracked reads, call services, and ask for a later operation. Its writes never join the turn that triggered it.

  • UI-only work uses the native UI lifetime.
  • App-session work uses an app-owned effect or mechanism.
  • Work promised after app shutdown needs saved input and an operating-system scheduler.

Swift registers mechanisms during app startup. Kotlin effect groups may belong to the app or a screen and close with that owner. This is a platform difference, not a change to the state/effect boundary.

UI updates

UI code reads Cog through its platform bridge. The bridge tracks the exact descriptor and key read by each UI scope. Only readers of a changed value update. If an automatic value stays equal, its UI readers do not update.

One app runtime does not mean one large view model or full-screen update. Features group declarations and operations. Views pass normal values and IDs. They do not copy Cog values into another writable UI model.

Swift bridges UI-read values to Observation. Kotlin reads a Compose version token attached to each UI-seen Cog state. The Cog runtime keeps the value; changing the token asks the exact Compose scopes that read it to recompose. The platform docs define exact tracking and lifetime mechanics.

Lifetime and ownership

The runtime owns graph storage for the app session. UI reads, effects, streams, and explicit hosts keep needed paths alive. When the last user leaves, automatic or async keyed state may wait for a grace period, cancel work, remove graph links, and release saved values.

Sources are kept longer because they are the source of truth. A source may reset when unused only if it clearly opts into that behavior. Query freshness, cache time, graph use, and app lifetime remain separate ideas.

Each test or preview is its own app runtime. It owns one isolated graph and shuts down that graph as one unit.

Identity and debugging

Runtime identity must stay stable for the life of a descriptor and key. A name shown to a person is separate. Source locations can help label debug output, but line numbers and stack traces are not stable identity.

Turn history and graph data should answer:

  • What operation started this turn?
  • Which sources changed?
  • Which automatic values ran, and why?
  • Which graph links changed?
  • Which equal results were dropped?
  • Which async jobs are active, replaced, failed, or complete?
  • Which UI readers, streams, reactions, or effects were told?

A release need not include a full debugger. Its internal choices should still keep enough data to build one. Do not put logging side effects in selectors.

Tests and migration

Tests use isolated runtimes with controlled time and completion events. They can set starting state, test automatic values, call operations, control async order, and check effect order and cleanup. Swift and Kotlin carry the same cross-platform scenarios and expected turn traces; platform suites add tests for their native adapters and physical representations.

Silent test setup is not a production write path. To test app startup, use the same mechanisms, effects, or startup inputs as production.

Migration tools may connect old observable state, streams, or data stores one piece at a time. Each bridge must say which side can write. Do not keep two writable copies in sync for the long term.

Platform choices

Each platform chooses its own:

  • ordered-lane primitive and off-lane execution tools;
  • public spelling where Swift and Kotlin idioms require it;
  • Observation, Compose, Flow, and AsyncSequence adapters;
  • physical storage, key, graph-link, and specialization representations;
  • dependency injection, saved state, and background-work integration; and
  • release plan and compatibility promises.

Those choices affect representation and syntax. The shared state machine, turn phases, observable results, and ownership rules stay fixed. Each platform records and tests its native choices against that model.

Released under the MIT License.