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 and real-time updates.
Prerequisites
- Completed Setup options
- A project configured in Console
When to use hybrid loading
- User-specific data – Preferences, cart items, session data
- Real-time updates – WebSocket data merged with static catalog
dcupl keeps all data in memory. Persisting runtime data (for example in localStorage) and syncing it back to a server is up to your application.
Basic pattern
Load from Console first, then add runtime data:
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 (process() without options loads every resource)
await loader.config.fetch();
await loader.process();
// 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([{ key: 'prefs', theme: 'dark', language: 'en' }], { model: 'UserPreferences' });
// 3. Initialize (init() doesn't fetch anything itself, so it runs after loader.process())
await dcupl.init();The sections below build on this instance. After the first init(), new models and data only need await dcupl.update(); don't call init() again.
Updating existing data
Use upsert() to merge fresh data with Console-loaded data:
// 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:
// After the initial load and init() from the Basic Pattern
// 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:
// Define user session model (runtime only)
dcupl.models.set({
key: 'CartItem',
properties: [
{ key: 'productKey', type: 'string' },
{ key: 'quantity', type: 'int' },
],
// origin: the reference reads the product key from the productKey field
references: [{ key: 'product', type: 'singleValued', model: 'Product', origin: 'productKey' }],
});
// Load from localStorage
const savedCart = JSON.parse(localStorage.getItem('cart') || '[]');
dcupl.data.set(savedCart, { model: 'CartItem' });
// The instance is already initialized: apply the new model and its data
await dcupl.update();
// Query cart with product details via reference
const cartList = dcupl.lists.create({ modelKey: 'CartItem' });
// A reference comes back as { key } by default; project it to get the product's fields
const cartItems = cartList.catalog.query.items({
projection: { $: true, product: { $: true } },
});
// [{ key: 'c1', productKey: 'p1', quantity: 2, product: { key: 'p1', name: 'Laptop', price: 999 } }, ...]Precedence rules
When the same item key exists in both Console and runtime data:
set()– Replaces all data of the model (runtime wins completely; items you don't pass are removed)update()– Merges fields into existing items (fields you send replace the stored values; unknown keys are ignored)upsert()– Same as update, but also adds new items
All three are staged until the next await dcupl.update().
To refresh the Console data later, process only the data resources, for example by tag, and apply them:
await loader.process({ resourceTags: ['data'] }); // only resources tagged 'data'
await dcupl.update();A plain loader.process() works too. It reloads the model files as well and rebuilds each model in place from them (properties are not duplicated, a changed type applies, a removed attribute disappears), and it drops the resources that left the loader configuration. The lists your app already holds, plain or scoped, serve the new data after await dcupl.update(), so you need no new lists or instance. Processing only the data resources by tag skips downloading the model files again. Models you defined in code, such as UserPreferences, keep their definition and rows.
Loader data resources are applied as set by default, so the refresh replaces every runtime change to those models, such as WebSocket prices. With options: { updateType: 'upsert' } on the resource, runtime items and attributes the resource doesn't contain stay, but the values it contains still overwrite yours. Either way, re-apply your overlay after a refresh (see Processing).
// Console has: { key: 'p1', name: 'Laptop', price: 999 }, plus p2 and p3
// This MERGES (keeps name from Console)
dcupl.data.update([{ key: 'p1', price: 899 }], { model: 'Product' });
await dcupl.update();
// p1: { key: 'p1', name: 'Laptop', price: 899 }
// This REPLACES all Product data (p1 loses name, p2 and p3 are removed)
dcupl.data.set([{ key: 'p1', price: 899 }], { model: 'Product' });
await dcupl.update();
// Products: [{ key: 'p1', price: 899 }]