Skip to content

State Model: Types, Collections, and Caching

How Verity manages truth-state with deterministic caching and staleness tracking

This guide explains Verity's state model—the internal structure that keeps truth-state cached, fresh, and synchronized.


The Big Picture

Verity's state model is built on a simple philosophy:

Truth-state lives in the cache. The cache is populated by fetches and kept fresh by directives.

┌─────────────────────────────────────┐
│          Verity Registry            │
│                                     │
│  ┌─────────────┐  ┌──────────────┐ │
│  │ Collections │  │    Types     │ │
│  │  (lists)    │  │ (individual) │ │
│  └─────────────┘  └──────────────┘ │
│                                     │
│  Populated by: fetch()              │
│  Kept fresh by: directives          │
└─────────────────────────────────────┘

Types: Individual Records

Types represent individual entities—a single user, a single invoice, a single product.

Basic Type Registration

<head>
  <script src="https://cdn.jsdelivr.net/npm/verity-dl@latest/verity/shared/static/lib/core.min.js"></script>
</head>

<script>
const registry = Verity.createRegistry()

registry.registerType('user', {
  fetch: async ({ id }) => {
    const response = await fetch(`/api/users/${id}`)
    return response.json()
  }
})
</script>

What this does: - Defines how to fetch a single user by ID - Verity will cache the result keyed by user:${id} - Subsequent requests for the same ID return cached data (if fresh)

Using Types in Your UI

<div x-data="{ user: $verity.item('user', 123) }">
  <template x-if="user.meta.loading">
    <p>Loading user...</p>
  </template>

  <template x-if="user.data">
    <div>
      <h2 x-text="user.data.name"></h2>
      <p x-text="user.data.email"></p>
    </div>
  </template>
</div>

Key concepts: - user.meta.loading - Is this item currently fetching? - user.meta.error - Did the fetch fail? - user.data - The actual data (null if not loaded yet) - user.meta.lastFetched - Timestamp of last successful fetch

Staleness Tracking

Cached data goes stale after a configured time:

registry.registerType('user', {
  fetch: async ({ id }) => {
    const response = await fetch(`/api/users/${id}`)
    return response.json()
  },
  stalenessMs: 5 * 60 * 1000  // 5 minutes
})

How it works: 1. First request fetches from server 2. Subsequent requests within 5 minutes use cached data 3. After 5 minutes, next request triggers refetch 4. Old data still shown until new data arrives (no flicker)


Collections: Lists of Records

Collections represent lists—all users, active invoices, recent orders.

Basic Collection Registration

registry.registerCollection('users', {
  fetch: async () => {
    const response = await fetch('/api/users')
    return response.json()  // Should return { items: [...], meta: {...} }
  }
})

Expected response format:

{
  "items": [
    { "id": 1, "name": "Alice" },
    { "id": 2, "name": "Bob" }
  ],
  "meta": {
    "total": 2,
    "page": 1
  }
}

Using Collections in Your UI

<div x-data="{ users: $verity.collection('users') }">
  <template x-if="users.state.loading">
    <p>Loading users...</p>
  </template>

  <ul>
    <template x-for="user in users.state.items" :key="user.id">
      <li x-text="user.name"></li>
    </template>
  </ul>

  <p>Total: <span x-text="users.state.meta.total"></span></p>
</div>

Key concepts: - users.state.loading - Is this collection currently fetching? - users.state.error - Did the fetch fail? - users.state.items - Array of items - users.state.meta - Metadata from server (pagination, totals, etc.)

Parameterized Collections

The killer feature: cache multiple "slices" of the same collection.

registry.registerCollection('users', {
  fetch: async (params = {}) => {
    const url = new URL('/api/users', location.origin)

    if (params.role) {
      url.searchParams.set('role', params.role)
    }
    if (params.status) {
      url.searchParams.set('status', params.status)
    }

    const response = await fetch(url)
    return response.json()
  },

  stalenessMs: 60 * 1000  // 1 minute
})

Using different parameter combinations:

<div x-data="{
  adminUsers: $verity.collection('users', { role: 'admin' }),
  activeUsers: $verity.collection('users', { status: 'active' }),
  allUsers: $verity.collection('users')
}">
  <!-- Each is cached separately! -->
</div>

Cache keys: - users:{} - All users - users:{"role":"admin"} - Just admins - users:{"status":"active"} - Just active users

Each combination is cached independently with its own staleness timer.


The Reference Model

When you access truth-state, Verity returns a reference (ref):

const userRef = registry.item('user', 123)

// Reference structure
{
  data: { id: 123, name: "Alice", email: "alice@example.com" },
  meta: {
    loading: false,
    error: null,
    lastFetched: 1234567890,
    activeQueryId: null
  }
}

Why references? - Stable objects that update in place - Frameworks can observe them for reactivity - Separates data from metadata

Collection References

const usersRef = registry.collection('users')

// Collection reference structure
{
  state: {
    items: [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }],
    meta: { total: 2, page: 1 },
    loading: false,
    error: null
  }
}

Cache Behavior

First Access: Triggers Fetch

// No cache, triggers fetch
const user = registry.item('user', 123)
// user.meta.loading === true
// user.data === null

// ... fetch completes ...
// user.meta.loading === false
// user.data === { id: 123, name: "Alice", ... }

Second Access: Uses Cache (if fresh)

// Within stalenessMs, uses cache
const user = registry.item('user', 123)
// user.meta.loading === false  ← No fetch!
// user.data === { id: 123, name: "Alice", ... }

After Staleness: Refetches

// After stalenessMs expires
const user = registry.item('user', 123)
// user.meta.loading === true  ← Refetch triggered
// user.data === { id: 123, name: "Alice", ... }  ← Old data still visible

// ... fetch completes ...
// user.data === { id: 123, name: "Alice (Updated)", ... }  ← New data

No flicker: Old data stays visible during refetch.


Directives: Server-Driven Invalidation

Instead of guessing when to refetch, the server tells us via directives.

Directive Flow

1. User updates something
2. Frontend sends mutation to server
3. Server updates database
4. Server returns directives
5. Frontend applies directives
6. Verity invalidates + refetches affected caches
7. UI updates automatically

Example: Update User

async function updateUser(id, data) {
  const response = await fetch(`/api/users/${id}`, {
    method: 'PUT',
    headers: { 
      'Content-Type': 'application/json',
      'X-Client-ID': registry.clientId 
    },
    body: JSON.stringify(data)
  })

  const payload = await response.json()

  // Server returned:
  // {
  //   "user": { ... },
  //   "directives": [
  //     { "op": "refresh_item", "name": "user", "id": 123 },
  //     { "op": "refresh_collection", "name": "users" }
  //   ]
  // }

  await registry.applyDirectives(payload.directives)
}

What happens: 1. refresh_item directive invalidates user:123 cache 2. Any components displaying that user refetch automatically 3. refresh_collection directive invalidates all users collections 4. Any user lists refetch automatically 5. UI updates when new data arrives

Learn more about Directives →


Levels: Detail Granularity

Sometimes you need different amounts of detail for the same entity.

registry.registerType('user', {
  // Default level: minimal data
  fetch: async ({ id }) => {
    const response = await fetch(`/api/users/${id}`)
    return response.json()
  },

  levels: {
    // Detailed level: includes more fields
    detailed: {
      fetch: async ({ id }) => {
        const response = await fetch(`/api/users/${id}?level=detailed`)
        return response.json()
      },
      checkIfExists: (obj) => 'bio' in obj && 'preferences' in obj
    }
  }
})

Usage:

<!-- List view: default level (id, name, email) -->
<div x-data="{ user: $verity.item('user', 123) }">
  <span x-text="user.data.name"></span>
</div>

<!-- Detail view: detailed level (adds bio, preferences, history) -->
<div x-data="{ user: $verity.item('user', 123, 'detailed') }">
  <h2 x-text="user.data.name"></h2>
  <p x-text="user.data.bio"></p>
  <!-- ... more details ... -->
</div>

Cache behavior: - user:123:default cached separately from user:123:detailed - Fetching detailed doesn't refetch default - Conversion graphs can derive one from another

Learn more about Levels & Conversions →


Memory Management

Long-lived apps can accumulate cached data. Verity includes automatic cleanup.

Default Behavior

const registry = Verity.createRegistry()
// Defaults:
// - Sweep every 60 seconds
// - Keep max 512 items per type
// - Keep max 12 parameter combos per collection
// - Expire items after 10 minutes of inactivity

Custom Configuration

const registry = Verity.createRegistry({
  memory: {
    enabled: true,
    sweepIntervalMs: 120_000,         // Sweep every 2 minutes
    maxItemsPerType: 1000,            // Keep up to 1000 users
    maxCollectionsPerName: 20,        // Keep up to 20 param combos
    itemTtlMs: 15 * 60 * 1000,        // Expire after 15 min
    collectionTtlMs: 10 * 60 * 1000   // Expire after 10 min
  }
})

How sweeping works: 1. Sweeper runs periodically 2. Removes least-recently-used items beyond max limits 3. Removes items not accessed within TTL 4. Never removes items with active fetches

Learn more about Concurrency & Memory →


Devtools

Inspect Verity's internal state in real-time:

<head>
  <script src="https://cdn.jsdelivr.net/npm/verity-dl@latest/verity/shared/static/lib/core.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/verity-dl@latest/verity/shared/static/devtools/devtools.min.js"></script>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/verity-dl@latest/verity/shared/static/devtools/devtools.css">
</head>

Devtools panels: - Truth: Current cache contents (types and collections) - Fetches: Active and recent fetch operations - Directives: Directive history and processing - SSE: Connection status and sequence tracking - Memory: Cache size and sweeper activity

Press Ctrl/Cmd + Shift + D or append ?devtools=1 to URL.


Best Practices

1. Appropriate Staleness Windows

// Frequently changing data: short staleness
registry.registerCollection('realtime_prices', {
  fetch: () => fetch('/api/prices').then(r => r.json()),
  stalenessMs: 5_000  // 5 seconds
})

// Rarely changing data: long staleness
registry.registerType('user_profile', {
  fetch: ({ id }) => fetch(`/api/users/${id}`).then(r => r.json()),
  stalenessMs: 10 * 60 * 1000  // 10 minutes
})

2. Return Rich Metadata from Collections

// ✅ GOOD: Include pagination, totals, filters
{
  "items": [...],
  "meta": {
    "total": 100,
    "page": 1,
    "perPage": 20,
    "hasMore": true
  }
}

// ❌ BAD: Just an array
[{ id: 1 }, { id: 2 }]

3. Use Directives Liberally

# After ANY mutation, return directives
@app.put('/api/users/<int:user_id>')
def update_user(user_id):
    # ... update database ...

    return {
        'user': user.to_dict(),
        'directives': [
            { 'op': 'refresh_item', 'name': 'user', 'id': user_id },
            { 'op': 'refresh_collection', 'name': 'users' }
        ]
    }

4. Separate Truth-State from View-State

<!-- ✅ GOOD: Clear separation -->
<div x-data="{
  users: $verity.collection('users'),  <!-- Truth-state -->
  sortBy: 'name',                       <!-- View-state -->
  sortOrder: 'asc',                     <!-- View-state -->
  selectedId: null                      <!-- View-state -->
}">

<!-- ❌ BAD: Mixed state -->
<div x-data="{
  users: [],           <!-- Truth-state but managed by Alpine? -->
  sortBy: 'name'       <!-- View-state -->
}">

Common Patterns

Pattern: Optimistic UI Prevention

// ❌ DON'T do this (optimistic update)
async function toggleUserActive(id) {
  // Optimistically toggle
  user.data.active = !user.data.active  // ← LYING TO USER

  await fetch(`/api/users/${id}/toggle`, { method: 'PUT' })
}

// ✅ DO this (honest loading state)
async function toggleUserActive(id) {
  isToggling = true  // Show spinner

  const res = await fetch(`/api/users/${id}/toggle`, { method: 'PUT' })
  const { directives } = await res.json()

  await registry.applyDirectives(directives)  // Server tells us new state
  isToggling = false
}

Pattern: Parameterized Filtering

// Register once with parameter support
registry.registerCollection('products', {
  fetch: async (params = {}) => {
    const url = new URL('/api/products', location.origin)
    if (params.category) url.searchParams.set('category', params.category)
    if (params.inStock !== undefined) url.searchParams.set('inStock', params.inStock)
    return fetch(url).then(r => r.json())
  }
})

// Use with different filters
<div x-data="{
  allProducts: $verity.collection('products'),
  electronicsInStock: $verity.collection('products', { category: 'electronics', inStock: true }),
  booksAll: $verity.collection('products', { category: 'books' })
}">

Pattern: Master-Detail

<!-- Master: List with default level -->
<ul>
  <template x-for="id in userIds">
    <li x-data="{ user: $verity.item('user', id) }">
      <button @click="selectedUserId = id">
        <span x-text="user.data?.name"></span>
      </button>
    </li>
  </template>
</ul>

<!-- Detail: Full data with detailed level -->
<div x-show="selectedUserId" x-data="{ user: $verity.item('user', selectedUserId, 'detailed') }">
  <h2 x-text="user.data?.name"></h2>
  <p x-text="user.data?.bio"></p>
  <!-- More details... -->
</div>

Summary

Verity's state model: - Types cache individual records by ID - Collections cache lists (with parameter support) - References provide stable, observable objects - Staleness tracking prevents unnecessary refetches - Directives from the server invalidate caches - Levels enable detail granularity - Memory management keeps cache bounded

The result: Deterministic, predictable caching where truth-state always reflects server reality.


Next Steps

  1. Understand Directives for server-driven invalidation
  2. Learn Levels & Conversions for efficient fetching
  3. Study Concurrency Model for correctness guarantees
  4. Review API Reference for complete configuration options