Hybrid Data Loading

Use the Loader for base configuration from Console, but add local models or data at runtime. This pattern is useful for user-specific data, real-time updates, and A/B testing.

Prerequisites

When to Use Hybrid Loading

  • User-specific data – Preferences, cart items, session data
  • Real-time updates – WebSocket data merged with static catalog
  • A/B testing – Override specific items for test groups
  • Local-first – Offline-capable apps with sync

Basic Pattern

Load from Console first, then add runtime data:

app.ts
import { Dcupl } from '@dcupl/core';
import { DcuplAppLoader } from '@dcupl/loader';

const dcupl = new Dcupl({
  config: {
    projectId: 'your-project-id',
    apiKey: 'your-api-key',
  },
});

const loader = new DcuplAppLoader();
dcupl.loaders.add(loader);

// 1. Load base data from Console
await loader.config.fetch();
await loader.process({ applicationKey: 'default' });

// 2. Add runtime-only model (not in Console)
dcupl.models.set({
  key: 'UserPreferences',
  properties: [
    { key: 'theme', type: 'string' },
    { key: 'language', type: 'string' },
  ],
});
dcupl.data.set(getUserPreferences(), { model: 'UserPreferences' });

// 3. Initialize
await dcupl.init();

Updating Existing Data

Use upsert() to merge fresh data with Console-loaded data:

app.ts
// Console loaded 1000 products
// API returns 50 updated products

const freshProducts = await fetch('/api/products/updates').then((r) => r.json());

// Upsert merges: updates existing, adds new
dcupl.data.upsert(freshProducts, { model: 'Product' });
await dcupl.update();

Real-Time Overlay

Add WebSocket updates on top of Console data:

app.ts
// Initial load from Console
await loader.config.fetch();
await loader.process({ applicationKey: 'default' });
await dcupl.init();

// Real-time updates via WebSocket
const ws = new WebSocket('wss://api.example.com/products');

ws.onmessage = async (event) => {
  const update = JSON.parse(event.data);

  if (update.type === 'price_change') {
    dcupl.data.update(
      [
        {
          key: update.productId,
          price: update.newPrice,
        },
      ],
      { model: 'Product' }
    );

    await dcupl.update();
  }
};

User Session Data

Keep user data separate from catalog data:

app.ts
// Define user session model (runtime only)
dcupl.models.set({
  key: 'CartItem',
  properties: [
    { key: 'productKey', type: 'string' },
    { key: 'quantity', type: 'int' },
  ],
  references: [{ key: 'product', model: 'Product', property: 'productKey' }],
});

// Load from localStorage
const savedCart = JSON.parse(localStorage.getItem('cart') || '[]');
dcupl.data.set(savedCart, { model: 'CartItem' });

await dcupl.init();

// Query cart with product details via reference
const cartList = dcupl.lists.create({ modelKey: 'CartItem' });
const cartItems = cartList.catalog.query.items();
// Each item has .product reference resolved

Precedence Rules

When the same item key exists in both Console and runtime data:

  1. set() – Replaces all data (runtime wins completely)
  2. update() – Merges fields (runtime fields override Console fields)
  3. upsert() – Same as update, but also adds new items
// Console has: { key: 'p1', name: 'Laptop', price: 999 }

// This MERGES (keeps name from Console)
dcupl.data.update([{ key: 'p1', price: 899 }], { model: 'Product' });
// Result: { key: 'p1', name: 'Laptop', price: 899 }

// This REPLACES (loses name)
dcupl.data.set([{ key: 'p1', price: 899 }], { model: 'Product' });
// Result: { key: 'p1', price: 899 }

Next Steps