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 IDv- 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 | nullUpsert
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 automaticallyPool vs Storage vs Graph
Don’t confuse these three:
| Concept | What It Is | Where | Persisted | Observable |
|---|---|---|---|---|
| ObjectPool | Flat map of WireRows | Memory | No | Yes (MobX) |
| Storage | Confirmed server data | Disk | Yes | No |
| Object Graph | Connected model instances | Memory | No | Yes (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 IssueRowWithout 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 fineFor 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
- Learn about Object Graph - Built from ObjectPool
- Learn about Deferred Persistence - Pool vs Storage
- Learn about Reactive Models - How MobX observability works
- See Data Pipeline - How data flows