Model
Base class for all Gluonic models. Provides core functionality for reactive, type-safe data models.
Import
import { Model } from '@gluonic/client'Class Signature
abstract class Model {
abstract id: string
// Properties
get isDeleted(): boolean
get exists(): boolean
get optimisticFields(): string[]
// Methods
isFieldOptimistic(field: string): boolean
async hydrate(depth?: number): Promise<Hydrated<this>>
// v1.1+ (Planned)
constructor(data?: RequiredFields<this> & Partial<OptionalFields<this>>)
async save(): Promise<void>
async delete(): Promise<void>
toJSON(): Record<string, any>
}Usage
Extend Model and use decorators to define your model:
import { Model, ClientModel, Property } from '@gluonic/client'
@ClientModel('user')
export class User extends Model {
@Property()
id = crypto.randomUUID()
@Property()
email!: string
@Property({ server: true })
serverCreatedAt?: number
}Properties
id
abstract id: stringUnique identifier for the model. Must be defined in your model class.
Example:
@Property()
id = crypto.randomUUID() // Auto-generatedisDeleted
get isDeleted(): booleanReturns true if this model has been deleted from the ObjectPool.
Behavior:
- Checks if the model still exists in
store.pool - Requires model to have store and type metadata set
- Returns
falseif metadata is missing
Example:
if (task.isDeleted) {
console.log('Task was deleted')
}exists
get exists(): booleanReturns true if this model still exists in the ObjectPool (opposite of isDeleted).
Example:
if (task.exists) {
// Safe to display task
return <TaskCard task={task} />
}optimisticFields
get optimisticFields(): string[]Returns array of field names that have pending optimistic updates.
Behavior:
- Returns empty array if no optimistic updates
- Fields are added when mutations are queued
- Fields are removed when server confirms
Example:
const hasOptimistic = task.optimisticFields.length > 0
// Show indicator if any fields are pending
if (hasOptimistic) {
return <PendingBadge fields={task.optimisticFields} />
}Methods
isFieldOptimistic()
isFieldOptimistic(field: string): booleanCheck if a specific field has a pending optimistic update.
Parameters:
| Parameter | Type | Description |
|---|---|---|
field | string | Field name to check |
Returns: boolean - true if field has pending update
Example:
const TitleEditor = observer(({ post }) => {
const isPending = post.isFieldOptimistic('title')
return (
<div>
<input value={post.title} />
{isPending && <Spinner />}
</div>
)
})hydrate()
async hydrate(depth: number = 1): Promise<Hydrated<this>>Hydrate all lazy references and collections up to specified depth.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
depth | number | 1 | How many levels deep to hydrate |
Returns: Promise<Hydrated<this>> - Model with hydrated relationships
Behavior:
depth = 1: Hydrates only direct lazy propertiesdepth > 1: Recursively hydrates nested modelsdepth < 1: Returns immediately without hydration- Sets
hydrated: trueflag on model
Hydrated Type:
type Hydrated<T extends Model> = T & {
[P in keyof T]: Required<T>[P] extends LazyReference<infer U>
? LazyReference<U> & { value: U }
: T[P]
} & { hydrated: true }Examples:
// Depth 1: Direct relationships only
const issue = useModel<Issue>('issue', issueId)
await issue.hydrate(1)
// issue.author.value is now loaded âś“
// issue.author.value.posts NOT loaded (depth 2)// Depth 2: Nested relationships
await issue.hydrate(2)
// issue.author.value.posts is now loaded âś“// Manual pre-loading
const IssueDetail = ({ issueId }) => {
const [loading, setLoading] = useState(true)
const issue = useModel<Issue>('issue', issueId)
useEffect(() => {
issue?.hydrate(2).then(() => setLoading(false))
}, [issue])
if (loading) return <Spinner />
// All relationships guaranteed loaded
return <div>{issue.author.value.name}</div>
}Instance Methods (v1.1+)
Planned Feature: These methods are planned for v1.1+ and not yet implemented. Currently, use store methods instead.
constructor() (v1.1+)
constructor(data?: RequiredFields<this> & Partial<OptionalFields<this>>)Create model instance with TypeScript-enforced required fields.
Type Inference:
@ClientModel('task')
class Task extends Model {
id = crypto.randomUUID() // Optional in constructor (has initializer)
title!: string // Required in constructor
description?: string // Optional in constructor
}
// TypeScript infers:
new Task(data: {
title: string // Required
id?: string // Optional (has initializer)
description?: string // Optional (has ?)
})Examples:
new Task({ title: 'Hello' }) // âś“
new Task({}) // ❌ Error: title required
new Task({ title: 'Hi', id: 'custom' }) // âś“ Override initializersave() (v1.1+)
async save(): Promise<void>Save model to sync engine. Automatically detects:
- New model → calls
store.create() - Existing model → calls
store.save()with changes
Behavior:
- Applies
defaultOn: 'update'defaults - Queues transaction
- Updates pool optimistically
- Syncs to server in background
Example:
const task = new Task({ title: 'New' })
await task.save() // Create
task.done = true
await task.save() // Update (updatedAt refreshed)delete() (v1.1+)
async delete(): Promise<void>Delete model from sync engine.
Behavior:
- Marks model as deleted in pool
- Queues delete transaction
- Syncs to server in background
Example:
await task.delete()
// After deletion
console.log(task.isDeleted) // true
console.log(task.exists) // falsetoJSON() (v1.1+)
toJSON(): Record<string, any>Extract all @Property fields as plain object.
Behavior:
- Returns object with all property values
- Excludes computed properties
- Excludes relationship fields
- Useful for serialization or logging
Example:
const json = task.toJSON()
// {
// id: 'abc-123',
// title: 'My Task',
// done: false,
// createdAt: 1234567890
// }Current API (v1.0)
Current: Use functional style with store methods.
import { useStore } from '@gluonic/client'
const MyComponent = () => {
const store = useStore()
// Create
const id = crypto.randomUUID()
await store.create('task', id, { title: 'New Task' })
// Update
await store.save('task', id, { done: true })
// Delete
await store.remove('task', id)
}Computed Properties
Add computed properties without decorators:
@ClientModel('post')
export class Post extends Model {
@Property()
content = ''
get wordCount(): number {
return this.content.split(/\s+/).length
}
get readTime(): string {
return `${Math.ceil(this.wordCount / 200)} min read`
}
get isLongForm(): boolean {
return this.wordCount > 1000
}
}
// In component
<Text>{post.readTime}</Text> // "5 min read"Custom Validation
Add custom validation methods:
@ClientModel('user')
export class User extends Model {
@Property()
email = ''
validate(): string | null {
if (!this.email.includes('@')) {
return 'Invalid email'
}
return null
}
}
// Usage
const error = user.validate()
if (error) {
alert(error)
} else {
await store.save('user', user.id, { email: user.email })
}See Also
- Decorators -
@ClientModel,@Property, relationship decorators - Store - Store methods (current API)
- Defining Models Guide - Complete model guide
- Reactive Models Concept - How reactivity works