Skip to Content

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: string

Unique identifier for the model. Must be defined in your model class.

Example:

@Property() id = crypto.randomUUID() // Auto-generated

isDeleted

get isDeleted(): boolean

Returns 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 false if metadata is missing

Example:

if (task.isDeleted) { console.log('Task was deleted') }

exists

get exists(): boolean

Returns 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): boolean

Check if a specific field has a pending optimistic update.

Parameters:

ParameterTypeDescription
fieldstringField 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:

ParameterTypeDefaultDescription
depthnumber1How many levels deep to hydrate

Returns: Promise<Hydrated<this>> - Model with hydrated relationships

Behavior:

  • depth = 1: Hydrates only direct lazy properties
  • depth > 1: Recursively hydrates nested models
  • depth < 1: Returns immediately without hydration
  • Sets hydrated: true flag 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 initializer

save() (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) // false

toJSON() (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

Last updated on