Messages

Understanding the Message model in Talon

Messages

Every change in Talon is represented as a Message. Understanding messages is key to understanding Talon.

What is a Message?

A message captures a single field-level change:

Message(
  id: 'msg-abc-123',           // Unique ID
  table: 'todos',              // Table name
  row: 'todo-456',             // Row ID (primary key)
  column: 'name',              // Column name
  value: 'Buy milk',           // New value (serialized)
  dataType: 'string',          // Type hint for deserialization
  localTimestamp: '00001704...',  // HLC timestamp
  serverTimestamp: 12345,      // Server-assigned order
  userId: 'user-789',          // Who made the change
  clientId: 'device-abc',      // Which device
  hasBeenSynced: false,        // Sync status
  hasBeenApplied: true,        // Applied to data table?
)

Message Fields

Identity Fields

FieldTypeDescription
idStringUnique identifier (usually UUID)
userIdStringThe user who made this change
clientIdStringThe device/client that made this change

Data Fields

FieldTypeDescription
tableStringTarget table name
rowStringRow identifier (primary key)
columnStringColumn/field name
valueStringSerialized value
dataTypeStringType hint for deserialization

Timestamp Fields

FieldTypeDescription
localTimestampStringHLC timestamp for conflict resolution
serverTimestampint?Server-assigned order for sync

Status Fields

FieldTypeDescription
hasBeenSyncedboolHas this been sent to the server?
hasBeenAppliedboolHas this been applied to the data table?

Creating Messages

You rarely create messages directly. Talon creates them when you call saveChange():

await talon.saveChange(
  table: 'todos',
  row: 'todo-1',
  column: 'name',
  value: 'Buy milk',
);

// Talon internally creates:
Message(
  id: uuid.v4(),
  table: 'todos',
  row: 'todo-1',
  column: 'name',
  value: 'Buy milk',
  dataType: 'string',
  localTimestamp: hlcState.send().toString(),
  serverTimestamp: null,
  userId: talon.userId,
  clientId: talon.clientId,
  hasBeenApplied: false,
  hasBeenSynced: false,
)

Value Serialization

All values are stored as strings. Talon handles serialization:

Dart TypedataTypeSerialized Value
null'null'''
String'string''Hello'
int'int''42'
double'double''3.14'
bool'bool''1' or '0'
DateTime'datetime''2024-01-15T10:30:00.000Z'
Map/List'json''{"key":"value"}'

Getting Typed Values

Use typedValue to deserialize:

final message = Message(..., dataType: 'int', value: '42');

final count = message.typedValue as int;  // 42

Message Lifecycle

1. Creation (Local Change)

await talon.saveChange(...);

// Message created with:
// - hasBeenSynced: false (needs sync)
// - hasBeenApplied: true (applied to data)

2. Sync to Server

// During sync:
await serverDb.sendMessageToServer(message: message);

// On success:
// - hasBeenSynced: true

3. Received from Server

// When receiving from server:

// If newer than existing:
// - hasBeenApplied: true
// - Applied to data table

// If older than existing:
// - hasBeenApplied: false
// - Stored but not applied

Serialization

To/From Map

// For database storage
final map = message.toMap();
await db.insert('talon_messages', map);

// From database
final rows = await db.query('talon_messages');
final messages = rows.map((r) => Message.fromMap(r)).toList();

To/From JSON

// For network transmission
final json = message.toJson();

// From JSON
final message = Message.fromJson(jsonString);

Message Comparison

Messages for the same cell (table/row/column) are compared by HLC timestamp:

final comparison = HLC.compareTimestamps(
  message1.localTimestamp,
  message2.localTimestamp,
);

if (comparison > 0) {
  // message1 is newer
} else if (comparison < 0) {
  // message2 is newer
}

Best Practices

Use Meaningful IDs

// Good: UUIDs
createNewIdFunction: () => uuid.v4(),

// Also good: Prefixed UUIDs
createNewIdFunction: () => 'msg-${uuid.v4()}',

Keep Values Small

Messages are stored individually. For large data:

// Instead of storing large JSON in one message:
await talon.saveChange(
  table: 'documents',
  row: 'doc-1',
  column: 'content',
  value: largeJsonObject,  // Could be very large
);

// Consider normalizing:
for (final section in document.sections) {
  await talon.saveChange(
    table: 'sections',
    row: section.id,
    column: 'content',
    value: section.content,
  );
}

Batch Related Changes

// Related changes should be batched:
await talon.saveChanges([
  TalonChangeData(table: 'todos', row: id, column: 'name', value: name),
  TalonChangeData(table: 'todos', row: id, column: 'updated_at', value: DateTime.now()),
]);

Next Steps