Real-time Data Updates

In this tutorial, you will learn how to handle real-time data updates in dcupl applications. You will build a live dashboard that reflects data changes instantly, using partial updates for performance and event subscriptions for reactivity.

Time required: 15-20 minutes

What you will build: A live inventory dashboard that updates in real-time as stock levels change, prices update, and new products are added.

Prerequisites

Before starting, make sure you have:

Why Real-time Updates Matter

Modern applications need to reflect data changes immediately:

  • Inventory systems - Stock levels change constantly
  • Pricing dashboards - Prices update in real-time
  • Collaborative tools - Multiple users edit simultaneously
  • Live feeds - New content appears automatically

dcupl provides three key mechanisms for handling this:

  1. Partial updates - Update only changed data (10-100x faster than full reload)
  2. Event subscriptions - React to data changes automatically
  3. List auto-updates - Keep query results synchronized

Step 1: Set Up the Project

Create a new project:

terminal
mkdir realtime-dashboard
cd realtime-dashboard
npm init -y
npm install @dcupl/core

Create the main application file:

dashboard.ts
import { Dcupl } from '@dcupl/core';

async function main() {
  const dcupl = new Dcupl();

  // We will build the real-time dashboard here
}

main();

Step 2: Define the Inventory Model

Create a model for tracking product inventory:

dashboard.ts
import { Dcupl } from '@dcupl/core';

async function main() {
  const dcupl = new Dcupl();

  // Define Inventory model
  dcupl.models.set({
    key: 'Inventory',
    properties: [
      { key: 'sku', type: 'string' },
      { key: 'name', type: 'string' },
      { key: 'category', type: 'string' },
      { key: 'price', type: 'float' },
      { key: 'stock', type: 'int' },
      { key: 'lowStockThreshold', type: 'int' },
      { key: 'lastUpdated', type: 'string' },
    ],
  });

  console.log('Inventory model defined');
}

main();

Step 3: Load Initial Data

Add initial inventory data:

dashboard.ts
// ... after dcupl.models.set() ...

// Load initial inventory
dcupl.data.set(
  [
    {
      key: 'inv1',
      sku: 'LAPTOP-001',
      name: 'MacBook Pro 14"',
      category: 'Electronics',
      price: 1999.99,
      stock: 25,
      lowStockThreshold: 10,
      lastUpdated: new Date().toISOString(),
    },
    {
      key: 'inv2',
      sku: 'LAPTOP-002',
      name: 'Dell XPS 15',
      category: 'Electronics',
      price: 1499.99,
      stock: 18,
      lowStockThreshold: 8,
      lastUpdated: new Date().toISOString(),
    },
    {
      key: 'inv3',
      sku: 'PHONE-001',
      name: 'iPhone 15 Pro',
      category: 'Electronics',
      price: 1199.99,
      stock: 45,
      lowStockThreshold: 20,
      lastUpdated: new Date().toISOString(),
    },
    {
      key: 'inv4',
      sku: 'CHAIR-001',
      name: 'Ergonomic Office Chair',
      category: 'Furniture',
      price: 399.99,
      stock: 12,
      lowStockThreshold: 5,
      lastUpdated: new Date().toISOString(),
    },
    {
      key: 'inv5',
      sku: 'DESK-001',
      name: 'Standing Desk',
      category: 'Furniture',
      price: 599.99,
      stock: 8,
      lowStockThreshold: 5,
      lastUpdated: new Date().toISOString(),
    },
    {
      key: 'inv6',
      sku: 'HEADPHONES-001',
      name: 'Sony WH-1000XM5',
      category: 'Electronics',
      price: 349.99,
      stock: 32,
      lowStockThreshold: 15,
      lastUpdated: new Date().toISOString(),
    },
  ],
  { model: 'Inventory' }
);

await dcupl.init();
console.log('Initial inventory loaded');

Step 4: Create the Inventory List

Create a list to query inventory data:

dashboard.ts
// ... after dcupl.init() ...

// Create inventory list
const inventoryList = dcupl.lists.create({
  modelKey: 'Inventory',
});

// Display dashboard
function displayDashboard() {
  console.clear();
  console.log('===========================================');
  console.log('         INVENTORY DASHBOARD');
  console.log('===========================================\n');

  const items = inventoryList.catalog.query.items({
    sort: { attributes: ['stock'], order: ['asc'] },
  });

  console.log('SKU            | Name                    | Stock | Price    | Status');
  console.log('---------------|-------------------------|-------|----------|--------');

  items.forEach((item) => {
    const status = item.stock <= item.lowStockThreshold ? 'LOW' : 'OK';
    const statusColor = status === 'LOW' ? '!!' : '  ';
    console.log(
      `${item.sku.padEnd(14)} | ` +
      `${item.name.slice(0, 23).padEnd(23)} | ` +
      `${String(item.stock).padStart(5)} | ` +
      `${item.price.toFixed(2).padStart(7)} | ` +
      `${statusColor}${status}`
    );
  });

  // Summary stats
  const totalStock = items.reduce((sum, i) => sum + i.stock, 0);
  const lowStockCount = items.filter((i) => i.stock <= i.lowStockThreshold).length;
  const totalValue = items.reduce((sum, i) => sum + i.stock * i.price, 0);

  console.log('\n-------------------------------------------');
  console.log(`Total Items: ${items.length} | Total Stock: ${totalStock} | Low Stock Alerts: ${lowStockCount}`);
  console.log(`Total Inventory Value: ${totalValue.toLocaleString()}`);
  console.log('-------------------------------------------\n');
}

displayDashboard();

Step 5: Implement Partial Updates with data.update()

The data.update() method updates existing items without replacing all data. This is much faster than data.set():

dashboard.ts
// Simulate a sale - reduce stock for one item
async function processSale(itemKey: string, quantity: number) {
  const item = inventoryList.catalog.query.one({ itemKey });
  if (!item) {
    console.log(`Item ${itemKey} not found`);
    return;
  }

  const newStock = Math.max(0, item.stock - quantity);

  // Use data.update() for partial update
  dcupl.data.update(
    [
      {
        key: itemKey,
        stock: newStock,
        lastUpdated: new Date().toISOString(),
      },
    ],
    { model: 'Inventory' }
  );

  // Process the update
  await dcupl.update();

  console.log(`Sale processed: ${item.name} - Sold ${quantity}, New stock: ${newStock}`);
}

// Test a sale
console.log('\n--- Processing Sale ---');
await processSale('inv1', 3);
displayDashboard();

Step 6: Subscribe to Data Changes with on()

Use the on() method to react to data changes:

dashboard.ts
// Subscribe to dcupl events
const unsubscribe = dcupl.on((event) => {
  console.log(`\n[Event] ${event.type}`);

  if (event.type === 'dcupl_updated_manually') {
    console.log('Data was updated - refreshing dashboard...');
    displayDashboard();
  }
});

// Now updates will trigger the callback
console.log('\n--- Subscribing to changes ---');
console.log('Dashboard will auto-refresh on data changes\n');

Step 7: Implement Upsert for New Items

Use data.upsert() to add new items or update existing ones:

dashboard.ts
// Add new inventory or update existing
async function upsertInventory(item: any) {
  dcupl.data.upsert([item], { model: 'Inventory' });
  await dcupl.update();
  console.log(`Upserted: ${item.name}`);
}

// Add a new product
console.log('\n--- Adding New Product ---');
await upsertInventory({
  key: 'inv7',
  sku: 'MONITOR-001',
  name: '4K Ultra Monitor',
  category: 'Electronics',
  price: 699.99,
  stock: 15,
  lowStockThreshold: 5,
  lastUpdated: new Date().toISOString(),
});

Step 8: Implement Batch Updates

Update multiple items at once for better performance:

dashboard.ts
// Batch price update (e.g., 10% discount on all electronics)
async function applyDiscount(category: string, discountPercent: number) {
  const items = inventoryList.catalog.query.items();
  const categoryItems = items.filter((i) => i.category === category);

  const updates = categoryItems.map((item) => ({
    key: item.key,
    price: Number((item.price * (1 - discountPercent / 100)).toFixed(2)),
    lastUpdated: new Date().toISOString(),
  }));

  if (updates.length > 0) {
    dcupl.data.update(updates, { model: 'Inventory' });
    await dcupl.update();
    console.log(`Applied ${discountPercent}% discount to ${updates.length} ${category} items`);
  }
}

console.log('\n--- Applying Batch Discount ---');
await applyDiscount('Electronics', 10);

Step 9: Handle Stock Replenishment

Simulate receiving new inventory:

dashboard.ts
// Replenish stock for low items
async function replenishLowStock() {
  const items = inventoryList.catalog.query.items();
  const lowStockItems = items.filter((i) => i.stock <= i.lowStockThreshold);

  if (lowStockItems.length === 0) {
    console.log('No items need replenishment');
    return;
  }

  const updates = lowStockItems.map((item) => ({
    key: item.key,
    stock: item.stock + 50, // Add 50 units
    lastUpdated: new Date().toISOString(),
  }));

  dcupl.data.update(updates, { model: 'Inventory' });
  await dcupl.update();

  console.log(`Replenished ${updates.length} low-stock items (+50 each)`);
}

console.log('\n--- Checking for Low Stock ---');
await replenishLowStock();

Step 10: Implement Real-time Simulation

Create a simulation that mimics real-world inventory changes:

dashboard.ts
// Simulate real-time inventory changes
function simulateRealTimeUpdates() {
  const operations = [
    async () => {
      // Random sale
      const items = inventoryList.catalog.query.items();
      const randomItem = items[Math.floor(Math.random() * items.length)];
      const qty = Math.floor(Math.random() * 5) + 1;
      await processSale(randomItem.key, qty);
    },
    async () => {
      // Random price change
      const items = inventoryList.catalog.query.items();
      const randomItem = items[Math.floor(Math.random() * items.length)];
      const priceChange = (Math.random() - 0.5) * 50; // +/- $25
      dcupl.data.update(
        [
          {
            key: randomItem.key,
            price: Number((randomItem.price + priceChange).toFixed(2)),
            lastUpdated: new Date().toISOString(),
          },
        ],
        { model: 'Inventory' }
      );
      await dcupl.update();
      console.log(`Price updated: ${randomItem.name}`);
    },
    async () => {
      // Stock arrival
      const items = inventoryList.catalog.query.items();
      const randomItem = items[Math.floor(Math.random() * items.length)];
      const arrival = Math.floor(Math.random() * 20) + 5;
      dcupl.data.update(
        [
          {
            key: randomItem.key,
            stock: randomItem.stock + arrival,
            lastUpdated: new Date().toISOString(),
          },
        ],
        { model: 'Inventory' }
      );
      await dcupl.update();
      console.log(`Stock arrival: ${randomItem.name} +${arrival}`);
    },
  ];

  // Run random operation every 2 seconds
  const interval = setInterval(async () => {
    const operation = operations[Math.floor(Math.random() * operations.length)];
    await operation();
  }, 2000);

  return () => clearInterval(interval);
}

// Start simulation
console.log('\n--- Starting Real-time Simulation ---');
console.log('Updates will occur every 2 seconds...\n');
const stopSimulation = simulateRealTimeUpdates();

// Stop after 10 seconds
setTimeout(() => {
  stopSimulation();
  console.log('\n--- Simulation Stopped ---');

  // Cleanup subscription
  unsubscribe();
  console.log('Unsubscribed from events');
}, 10000);

Step 11: Implement Remove and Reset

Handle item removal and full data reset:

dashboard.ts
// Remove discontinued items
async function removeItem(itemKey: string) {
  dcupl.data.remove([{ key: itemKey }], { model: 'Inventory' });
  await dcupl.update();
  console.log(`Removed item: ${itemKey}`);
}

// Reset all inventory (e.g., end of year)
async function resetInventory(newData: any[]) {
  // data.set() replaces ALL data - use sparingly
  dcupl.data.set(newData, { model: 'Inventory' });
  await dcupl.update();
  console.log('Inventory completely reset');
}

Step 12: Enable Auto-updating Lists

Configure lists to automatically update when data changes:

dashboard.ts
// Option 1: Enable auto-update for a specific list
const autoList = dcupl.lists.create({
  modelKey: 'Inventory',
  autoUpdate: true, // This list automatically refreshes on data changes
});

// Option 2: Enable globally for all lists during dcupl creation
const dcuplWithAutoUpdate = new Dcupl({
  performance: {
    autoUpdateLists: {
      enabled: true, // All lists auto-update by default
    },
  },
});

// Option 3: Override auto-update behavior per update() call
await dcupl.update({ autoUpdateLists: true }); // Force update all lists
await dcupl.update({ autoUpdateLists: false }); // Skip auto-update for all lists

Complete Code

Here is the complete real-time dashboard:

dashboard.ts
import { Dcupl } from '@dcupl/core';

async function main() {
  const dcupl = new Dcupl();

  // Define model
  dcupl.models.set({
    key: 'Inventory',
    properties: [
      { key: 'sku', type: 'string' },
      { key: 'name', type: 'string' },
      { key: 'category', type: 'string' },
      { key: 'price', type: 'float' },
      { key: 'stock', type: 'int' },
      { key: 'lowStockThreshold', type: 'int' },
      { key: 'lastUpdated', type: 'string' },
    ],
  });

  // Load data
  dcupl.data.set(
    [
      { key: 'inv1', sku: 'LAPTOP-001', name: 'MacBook Pro 14"', category: 'Electronics', price: 1999.99, stock: 25, lowStockThreshold: 10, lastUpdated: new Date().toISOString() },
      { key: 'inv2', sku: 'LAPTOP-002', name: 'Dell XPS 15', category: 'Electronics', price: 1499.99, stock: 18, lowStockThreshold: 8, lastUpdated: new Date().toISOString() },
      { key: 'inv3', sku: 'PHONE-001', name: 'iPhone 15 Pro', category: 'Electronics', price: 1199.99, stock: 45, lowStockThreshold: 20, lastUpdated: new Date().toISOString() },
      { key: 'inv4', sku: 'CHAIR-001', name: 'Ergonomic Office Chair', category: 'Furniture', price: 399.99, stock: 12, lowStockThreshold: 5, lastUpdated: new Date().toISOString() },
      { key: 'inv5', sku: 'DESK-001', name: 'Standing Desk', category: 'Furniture', price: 599.99, stock: 8, lowStockThreshold: 5, lastUpdated: new Date().toISOString() },
    ],
    { model: 'Inventory' }
  );

  await dcupl.init();
  const inventoryList = dcupl.lists.create({ modelKey: 'Inventory' });

  // Display function
  function displayDashboard() {
    console.log('\n=== INVENTORY DASHBOARD ===\n');
    const items = inventoryList.catalog.query.items({ sort: { attributes: ['stock'], order: ['asc'] } });
    items.forEach((item) => {
      const status = item.stock <= item.lowStockThreshold ? '[LOW]' : '[OK] ';
      console.log(`${status} ${item.name}: ${item.stock} units @ ${item.price}`);
    });
    console.log(`\nTotal: ${items.length} products, ${items.reduce((s, i) => s + i.stock, 0)} units`);
  }

  // Subscribe to updates
  const unsubscribe = dcupl.on((event) => {
    if (event.type === 'dcupl_updated_manually') {
      displayDashboard();
    }
  });

  // Initial display
  displayDashboard();

  // Demonstrate updates
  console.log('\n--- Simulating sale of 5 MacBooks ---');
  dcupl.data.update([{ key: 'inv1', stock: 20, lastUpdated: new Date().toISOString() }], { model: 'Inventory' });
  await dcupl.update();

  console.log('\n--- Adding new product ---');
  dcupl.data.upsert([{ key: 'inv6', sku: 'MONITOR-001', name: '4K Monitor', category: 'Electronics', price: 699.99, stock: 15, lowStockThreshold: 5, lastUpdated: new Date().toISOString() }], { model: 'Inventory' });
  await dcupl.update();

  console.log('\n--- Batch price update (10% off Electronics) ---');
  const electronics = inventoryList.catalog.query.items().filter((i) => i.category === 'Electronics');
  const priceUpdates = electronics.map((i) => ({ key: i.key, price: Number((i.price * 0.9).toFixed(2)) }));
  dcupl.data.update(priceUpdates, { model: 'Inventory' });
  await dcupl.update();

  // Cleanup
  unsubscribe();
}

main();

What You Learned

In this tutorial, you learned how to:

  1. Use data.update() for partial updates that only modify changed fields
  2. Use data.upsert() to insert or update items in a single operation
  3. Subscribe to dcupl events with on() for reactive updates
  4. Process batch updates efficiently
  5. Handle data.remove() for deleting items
  6. Use data.set() for complete data replacement (use sparingly)
  7. Configure auto-updating lists for automatic synchronization

Data Methods Summary

Method Purpose Performance Use Case
data.update() Update existing items Fast (partial) Stock changes, price updates
data.upsert() Insert or update Fast New items or sync from external source
data.set() Replace all data Slow (full reload) Initial load, complete refresh
data.remove() Delete items Fast Discontinued products
data.reset() Clear all data Fast Clear model before reload

Update Workflow

// 1. Make data changes (staged, not applied yet)
dcupl.data.update([...], { model: 'MyModel' });
dcupl.data.upsert([...], { model: 'MyModel' });
dcupl.data.remove([...], { model: 'MyModel' });

// 2. Apply all changes at once
await dcupl.update();

// 3. Event subscribers are notified
// 4. Auto-updating lists refresh automatically

Next Steps

Common Issues

Updates not reflecting in UI

Make sure you call await dcupl.update() after making data changes. Data modifications are staged until you call update().

Slow performance with large updates

Use partial updates (data.update()) instead of full replacement (data.set()) whenever possible. For batch updates, combine multiple changes into a single data.update() call.

Event handler called multiple times

Each call to on() creates a new subscription. Store the unsubscribe function and call it when the component unmounts or before creating a new subscription.

// Store reference
const unsubscribe = dcupl.on(callback);

// Later: cleanup
unsubscribe();

List not updating automatically

Enable auto-update when creating the list:

const list = dcupl.lists.create({
  modelKey: 'MyModel',
  autoUpdate: true,
});

Or manually call list.update() after dcupl.update().