Skip to Content

ObjectPool

Concept: In-memory MobX observable map that holds all current data.

What is ObjectPool?

ObjectPool is the central in-memory storage in Gluonic’s architecture:

  • MobX observable - Changes trigger component re-renders
  • Flat map structure - Stores WireRows by type:id keys
  • Single source of truth - All current state lives here
  • Optimistic + Confirmed - Holds both types of data
  • Never persisted - Lives only in memory

Structure

class ObjectPool { map = observable.map<string, WireRow>() get(t: string, id: string): WireRow | null upsert(row: WireRow): void delete(t: string, id: string): void }

Internal storage format:

ObjectPool.map = Map { 'user:alice' → { t: 'user', id: 'alice', v: 5, p: { name: 'Alice', email: '...' } } 'issue:123' → { t: 'issue', id: '123', v: 12, p: { title: 'Bug', teamId: '1' } } 'team:1' → { t: 'team', id: '1', v: 3, p: { name: 'Engineering' } } }

Each entry is a WireRow with:

  • t - Type name (e.g., ‘user’, ‘issue’)
  • id - Record ID
  • v - Version number (for conflict detection)
  • p - Payload (field values)

Role in Architecture

┌─────────────────────────────────────────┐ │ React Components │ │ Read data via Object Graph │ └────────────────┬────────────────────────┘ │ IdentityMap builds... ┌─────────────────────────────────────────┐ │ Object Graph │ │ Connected model instances │ │ (team.issues, issue.assignee, etc.) │ └────────────────┬────────────────────────┘ │ Reads from... ┌─────────────────────────────────────────┐ │ ObjectPool (In-Memory) ◄──┼── You are here │ Flat map of WireRows │ │ MobX observable │ └────────────────┬────────────────────────┘ │ Synced with... ┌─────────────────────────────────────────┐ │ Local Storage (Drizzle) │ │ Confirmed server data only │ └─────────────────────────────────────────┘

ObjectPool sits between the Object Graph (what you use) and Storage (where confirmed data persists).

Why Flat Storage?

ObjectPool stores data as a flat map, not as an Object Graph:

// ObjectPool (flat) { 'issue:123': { t: 'issue', id: '123', p: { teamId: '1', assigneeId: 'alice' } } 'team:1': { t: 'team', id: '1', p: { name: 'Engineering' } } 'user:alice': { t: 'user', id: 'alice', p: { name: 'Alice' } } } // Object Graph (connected) - built from pool { issue: Issue { id: '123', team: Reference<Team>, // Points to team:1 assignee: LazyReference<User> // Points to user:alice } }

Why flat?

  • Simpler to sync (just add/update/delete rows)
  • No circular reference issues in storage
  • Easy to serialize for persistence
  • Graph structure is derived, not stored

MobX Observability

ObjectPool is a MobX observable - this enables reactivity:

class ObjectPool { map = observable.map<string, WireRow>({}, { deep: false }) }

When data changes:

// Update pool store.pool.upsert({ t: 'issue', id: '123', p: { title: 'New Title' } }) // MobX detects change // Finds components reading issue:123.title // Triggers re-render automatically ✓

Optimistic vs Confirmed Data

ObjectPool holds BOTH optimistic and confirmed data:

// Initial state (confirmed) pool.get('issue', '123') // → { t: 'issue', id: '123', v: 5, p: { title: 'Old Title' } } // User makes optimistic update await store.save('issue', '123', { title: 'New Title' }) // Pool updated immediately (optimistic) pool.get('issue', '123') // → { t: 'issue', id: '123', v: 5, p: { title: 'New Title' } } // Storage still has old value (confirmed) storage.getRow('issue', '123') // → { t: 'issue', id: '123', v: 5, p: { title: 'Old Title' } } // After server confirms... // Pool: { title: 'New Title' } (same) // Storage: { title: 'New Title' } (now updated)

This is Deferred Persistence - optimistic updates live in pool, confirmed data in storage.

Pool Operations

Get

const row = store.pool.get('issue', '123') // Returns: WireRow | null

Upsert

store.pool.upsert({ t: 'issue', id: '123', v: 6, p: { title: 'Updated', content: 'New content' } }) // Merges with existing data (preserves other fields)

Delete

store.pool.delete('issue', '123') // Removes from pool // Triggers MobX reactions (components re-render)

Query by Type

const allIssues = store.pool.entriesByType('issue') // Returns: WireRow[]

Query by Index

const teamIssues = store.pool.entriesByIndex('issue', 'teamId', 'team-1') // Returns: WireRow[] where teamId = 'team-1'

Lifecycle

1. Bootstrap

// Initial load - populate pool from server const rows = await fetch('/sync/v1/bootstrap') store.pool.upsertMany(rows) // Add all to pool // Also write to storage for offline access await storage.putRows(rows)

2. Delta Sync

// Incremental updates const frames = await fetch('/sync/v1/delta?since=1000') for (const frame of frames) { if (frame.op === 'i' || frame.op === 'u') { store.pool.upsert({ t: frame.t, id: frame.id, p: frame.p }) } else if (frame.op === 'd') { store.pool.delete(frame.t, frame.id) } }

3. Optimistic Updates

// User makes change await store.save('issue', '123', { title: 'New' }) // Pool updated immediately (optimistic) store.pool.upsert({ t: 'issue', id: '123', p: { title: 'New' } }) // Queue for server (not yet in storage)

4. Rollback

// Server rejects optimistic update // Restore previous value from transaction queue store.pool.upsert({ t: 'issue', id: '123', p: previousData }) // MobX triggers re-render // UI reverts automatically

Pool vs Storage vs Graph

Don’t confuse these three:

ConceptWhat It IsWherePersistedObservable
ObjectPoolFlat map of WireRowsMemoryNoYes (MobX)
StorageConfirmed server dataDiskYesNo
Object GraphConnected model instancesMemoryNoYes (via pool)

Data flow:

Storage (disk) ↓ Load ObjectPool (memory) ↓ Build Object Graph (connected models) ↓ Access Components (UI)

Why ObjectPool is Observable

Making ObjectPool observable is what enables Gluonic’s reactive models:

const IssueRow = observer(({ issue }) => { return <div>{issue.title}</div> }) // Behind the scenes: // 1. Component reads issue.title // 2. Proxy trap: reads from pool.get('issue', '123').p.title // 3. MobX tracks: "IssueRow depends on pool['issue:123'].p.title" // Later, when data changes: store.pool.upsert({ t: 'issue', id: '123', p: { title: 'Updated' } }) // 4. MobX detects: "pool['issue:123'] changed" // 5. MobX finds: "IssueRow depends on this" // 6. MobX triggers: Re-render IssueRow

Without observable pool, components wouldn’t automatically update.

Memory Management

Garbage Collection

ObjectPool uses standard JavaScript Map - subject to GC:

// Models are removed from pool when deleted store.pool.delete('issue', '123') // Components stop referencing it // Eventually garbage collected ✓

Pool Size

For large datasets, pool can grow:

// 10,000 issues × 2KB each = 20MB in pool // Modern devices handle this fine

For massive datasets (100k+ objects), use lazy loading aggressively - only load what’s needed into the pool.

Direct Pool Access (Rare)

You rarely access pool directly (use Object Graph instead):

// ❌ Don't do this (bypasses Object Graph) const raw = store.pool.get('issue', '123') console.log(raw.p.title) // ✅ Do this (uses Object Graph) const issue = useModel<Issue>('issue', '123') console.log(issue.title)

Object Graph provides:

  • Identity mapping
  • Type safety
  • Lazy loading
  • Relationship navigation

API Reference: ObjectPool · Store.pool

Next Steps

Last updated on