What are Events?

Events are the fundamental building blocks of kSync. They represent immutable facts about what happened in your application. Instead of directly modifying state, you create events that describe the changes.

Immutable

Events are never modified once created, providing a reliable audit trail

Type-Safe

Every event is validated against a Zod schema at runtime

Timestamped

Events include precise timestamps for ordering and conflict resolution

Traceable

Full provenance tracking with client ID and version numbers

Event Structure

Every kSync event follows this structure:
interface KSyncEvent<T = unknown> {
  id: string;           // Unique identifier (UUID)
  type: string;         // Event type (e.g., 'message-sent')
  data: T;              // Event payload (your data)
  timestamp: number;    // Unix timestamp in milliseconds
  clientId: string;     // ID of the client that created the event
  version: number;      // Sequence number for ordering
}

Example Event

{
  id: "evt_1703123456789_abc123",
  type: "message-sent",
  data: {
    id: "msg_001",
    content: "Hello, world!",
    author: "alice",
    channelId: "general"
  },
  timestamp: 1703123456789,
  clientId: "client_alice_browser",
  version: 42
}

Defining Event Schemas

Use Zod schemas to define the structure and validation rules for your events:

Basic Schema

import { z } from 'zod';
import { createKSync } from '@klastra/ksync';

const ksync = createKSync();

// Define a message schema
const MessageSchema = z.object({
  id: z.string(),
  content: z.string().min(1).max(1000),
  author: z.string(),
  channelId: z.string(),
});

// Register the schema
ksync.defineSchema('message-sent', MessageSchema);

Advanced Schema Features

const UserSchema = z.object({
  id: z.string().uuid(),                    // Must be valid UUID
  username: z.string()
    .min(3, "Username too short")
    .max(20, "Username too long")
    .regex(/^[a-zA-Z0-9_]+$/, "Invalid characters"),
  email: z.string().email(),               // Valid email format
  age: z.number().int().min(13).max(120),  // Integer between 13-120
  roles: z.array(z.enum(['user', 'admin', 'moderator'])),
  metadata: z.record(z.string()).optional(), // Optional key-value pairs
});

ksync.defineSchema('user-created', UserSchema);

Sending Events

Once you’ve defined schemas, you can send type-safe events:

Basic Event Sending

// Send a message event
await ksync.send('message-sent', {
  id: 'msg_001',
  content: 'Hello, world!',
  author: 'alice',
  channelId: 'general',
});

// TypeScript will catch errors at compile time
await ksync.send('message-sent', {
  id: 'msg_002',
  content: 123, // ❌ TypeScript error: should be string
  author: 'bob',
  channelId: 'general',
});

Batch Event Sending

For better performance, kSync automatically batches events:
// These events will be batched together
await Promise.all([
  ksync.send('user-joined', { userId: 'user1', channelId: 'general' }),
  ksync.send('user-joined', { userId: 'user2', channelId: 'general' }),
  ksync.send('user-joined', { userId: 'user3', channelId: 'general' }),
]);

Error Handling

try {
  await ksync.send('message-sent', {
    id: 'msg_003',
    content: '', // ❌ Violates min length rule
    author: 'charlie',
    channelId: 'general',
  });
} catch (error) {
  if (error instanceof z.ZodError) {
    console.error('Validation failed:', error.errors);
  }
}

Listening to Events

Set up event listeners to react to events as they happen:

Basic Event Listeners

// Listen for message events
ksync.on('message-sent', (event) => {
  const { content, author, channelId } = event.data;
  console.log(`${author} in #${channelId}: ${content}`);
  
  // Update your UI
  addMessageToChannel(channelId, event.data);
});

// Listen for user events
ksync.on('user-joined', (event) => {
  const { userId, channelId } = event.data;
  console.log(`User ${userId} joined #${channelId}`);
  
  // Update user list
  addUserToChannel(channelId, userId);
});

Type-Safe Event Handlers

// Define typed event handlers
type MessageEvent = z.infer<typeof MessageSchema>;

const handleMessage = (event: KSyncEvent<MessageEvent>) => {
  // TypeScript knows the exact shape of event.data
  const { id, content, author, channelId } = event.data;
  
  // Your handler logic here
  updateMessageUI(event.data);
};

ksync.on('message-sent', handleMessage);

Removing Event Listeners

// Remove specific listener
ksync.off('message-sent', handleMessage);

// Remove all listeners for an event type
ksync.off('message-sent');

Event Patterns

Command Events

Events that represent user actions or commands:
// User actions
ksync.defineSchema('message-send', z.object({
  content: z.string(),
  channelId: z.string(),
}));

ksync.defineSchema('user-join-channel', z.object({
  userId: z.string(),
  channelId: z.string(),
}));

ksync.defineSchema('file-upload', z.object({
  fileName: z.string(),
  fileSize: z.number(),
  mimeType: z.string(),
  uploadUrl: z.string(),
}));

State Change Events

Events that represent changes to application state:
// State changes
ksync.defineSchema('message-edited', z.object({
  messageId: z.string(),
  newContent: z.string(),
  editedAt: z.number(),
}));

ksync.defineSchema('user-status-changed', z.object({
  userId: z.string(),
  status: z.enum(['online', 'away', 'busy', 'offline']),
  lastSeen: z.number(),
}));

ksync.defineSchema('channel-settings-updated', z.object({
  channelId: z.string(),
  settings: z.object({
    name: z.string().optional(),
    description: z.string().optional(),
    isPrivate: z.boolean().optional(),
  }),
}));

System Events

Events generated by the system or external services:
// System events
ksync.defineSchema('user-authenticated', z.object({
  userId: z.string(),
  sessionId: z.string(),
  loginMethod: z.enum(['password', 'oauth', 'sso']),
}));

ksync.defineSchema('backup-completed', z.object({
  backupId: z.string(),
  timestamp: z.number(),
  size: z.number(),
  checksum: z.string(),
}));

ksync.defineSchema('rate-limit-exceeded', z.object({
  userId: z.string(),
  action: z.string(),
  limit: z.number(),
  resetTime: z.number(),
}));

Event Versioning

As your application evolves, you may need to change event schemas:

Schema Evolution

// Version 1
const MessageV1Schema = z.object({
  id: z.string(),
  content: z.string(),
  author: z.string(),
});

// Version 2 - Add optional fields
const MessageV2Schema = z.object({
  id: z.string(),
  content: z.string(),
  author: z.string(),
  channelId: z.string().optional(), // New optional field
  mentions: z.array(z.string()).optional(), // New optional field
});

// Version 3 - Make channelId required
const MessageV3Schema = z.object({
  id: z.string(),
  content: z.string(),
  author: z.string(),
  channelId: z.string(), // Now required
  mentions: z.array(z.string()).optional(),
  attachments: z.array(z.object({
    id: z.string(),
    url: z.string(),
    type: z.string(),
  })).optional(), // New optional field
});

Migration Strategy

// Handle multiple schema versions
const handleMessageEvent = (event: KSyncEvent) => {
  // Try parsing with latest schema first
  try {
    const data = MessageV3Schema.parse(event.data);
    // Handle V3 format
    return handleMessageV3(data);
  } catch {
    try {
      const data = MessageV2Schema.parse(event.data);
      // Handle V2 format, migrate to V3
      return handleMessageV2(data);
    } catch {
      const data = MessageV1Schema.parse(event.data);
      // Handle V1 format, migrate to V3
      return handleMessageV1(data);
    }
  }
};

Best Practices

Performance Considerations

Event Size

Keep events reasonably sized for optimal performance:
// ✅ Good - Lightweight event
const MessageSchema = z.object({
  id: z.string(),
  content: z.string().max(2000), // Reasonable limit
  author: z.string(),
  channelId: z.string(),
});

// ❌ Bad - Heavy event
const MessageSchema = z.object({
  id: z.string(),
  content: z.string(), // No size limit
  author: z.object({
    // Entire user object embedded
    id: z.string(),
    profile: z.object({
      avatar: z.string(), // Base64 image data
      preferences: z.record(z.unknown()),
      // ... lots more data
    }),
  }),
});

Validation Performance

Zod schemas are fast, but complex validations can impact performance:
// ✅ Good - Simple, fast validation
const SimpleSchema = z.object({
  id: z.string(),
  value: z.number(),
});

// ⚠️ Careful - Complex validation
const ComplexSchema = z.object({
  id: z.string().refine(async (id) => {
    // Async validation can be slow
    return await checkIdExists(id);
  }),
  data: z.array(z.object({
    // Nested arrays can be expensive to validate
    items: z.array(z.object({
      value: z.string().transform((val) => {
        // Complex transformations add overhead
        return expensiveTransform(val);
      }),
    })),
  })),
});

Next Steps