Documentation

Overview

The Collection module consists of several highly optimized factory functions that work together to provide a complete data management solution:

  • createCollection: A lightweight factory function that creates a collection with support for querying, sorting, and observing changes.
  • createListManager: A utility for handling paginated data with virtualized rendering.
  • createRouteAdapter: An adapter for integrating with REST APIs, handling serialization, parameters, and caching.
  • createBaseAdapter: A common foundation for all adapters with error handling.

These components can be used independently or combined to create powerful data management solutions for your applications while maintaining excellent performance characteristics.

Core Components

createCollection

The Collection module provides a functional approach to data management through the createCollection factory function. It offers a consistent interface for working with collections of data, managing items with unique IDs, providing filtering and sorting capabilities, and supporting observers for reacting to changes.

This highly optimized implementation includes result caching, batch processing, and memory efficiency features to ensure excellent performance even with large datasets:

  • Add, update, and remove items with optimized batch operations
  • Smart filtering and sorting with result caching
  • Subscribe to collection changes with efficient notification system
  • Type-safe operations with generics
  • Error and loading state handling
  • Minimal memory footprint and optimized event notification

createListManager

The createListManager builds on top of the optimized Collection factory to provide specialized handling for paginated lists with virtual rendering, including:

  • Memory-efficient cursor-based pagination
  • Virtual list rendering for performance with minimal DOM operations
  • Smart recycling of DOM elements for better memory usage
  • Automatic loading of new pages on scroll with configurable thresholds
  • Intelligent visibility detection using hybrid scrolling strategy
  • Page history for navigation
  • Seamless integration with DOM elements

createRouteAdapter

The createRouteAdapter provides lightweight connectivity to REST APIs with optimized features like:

  • Minimal-overhead URL building and query parameter handling with reused objects
  • Efficient response parsing with specialized error handling
  • Intelligent request caching to reduce network calls
  • Optimized support for cursor-based pagination
  • Streamlined transformation of complex query conditions into URL parameters
  • Automatic abort of superseded requests

createBaseAdapter

The createBaseAdapter is a minimalist foundational component providing common functionality for all adapters with minimal overhead:

  • Streamlined error handling with context preservation
  • Efficient error reporting
  • Minimal base interface implementation
  • Zero dependencies and small footprint

Detailed API Reference

Collection API

Creating a Collection

import { createCollection } from 'core/collection';

// Define your item type
interface User {
  id: string;
  name: string;
  email: string;
  role: string;
}

// Create a collection with performance optimization options
const users = createCollection<User>({
  // Transform function to normalize data
  transform: (data) => ({
    id: data._id || data.id,
    name: data.name || 'Unknown',
    email: data.email || '',
    role: data.role || 'user'
  }),
  // Validation function for data integrity
  validate: (item) => Boolean(item.id && item.email),
  // Optional capacity hint for large collections
  initialCapacity: 1000
});

Collection Methods

MethodDescriptionPerformance Notes
subscribe(observer)Subscribe to collection changes, returns unsubscribe functionOptimized notification system that reuses objects
getItems()Get collection items based on current query and sortUses result caching for repeated calls
getSize()Get collection sizeO(1) operation using Map size
isLoading()Get loading stateSimple boolean flag access
getError()Get error stateDirect reference access
query(queryFn)Set query filter functionInvalidates cache intelligently
sort(sortFn)Set sort functionUses optimized sorting with result caching
add(items)Add items to collection, returns added itemsBatch processing for multiple items
update(items)Update items in collection, returns updated itemsOptimized with validation check before updates
remove(ids)Remove items from collection, returns removed item IDsBatch deletion with minimal overhead
clear()Clear all items from collectionEfficient reset of internal state

Collection Events

EventDescription
changeFired when collection items change
addFired when items are added
updateFired when items are updated
removeFired when items are removed
errorFired when an error occurs
loadingFired when loading state changes

List Manager API

Creating a List Manager

import { createListManager } from 'core/collection';

const listManager = createListManager('users', containerElement, {
  // Item rendering function (required)
  renderItem: (user, index) => {
    const el = document.createElement('div');
    el.className = 'user-item';
    el.innerHTML = `<h3>${user.name}</h3><p>${user.email}</p>`;
    return el;
  },
  
  // Optional configuration
  transform: (data) => ({
    id: data._id,
    name: data.name || 'Unknown',
    email: data.email || '',
    meta: data.role
  }),
  baseUrl: 'https://api.example.com/api',
  itemHeight: 60,
  pageSize: 25,
  afterLoad: ({ loading, hasNext, items }) => {
    // Update UI or emit events after data loads
    updateLoadingIndicator(loading);
  }
});

List Manager Methods

MethodDescription
loadItems(params)Load items with parameters, returns response
loadMore()Load next page of items, returns result
refresh()Refresh list with latest data
updateVisibleItems()Update visible items based on scroll position
scrollToItem(itemId, position)Scroll to specific item by ID
setItemHeights(heightsMap)Set custom heights for specific items
getCollection()Get the underlying Collection instance
getVisibleItems()Get currently visible items
getAllItems()Get all loaded items
isLoading()Check if items are currently loading
hasNextPage()Check if there are more items to load
isApiMode()Check if manager is in API mode
setRenderHook(hookFn)Set hook function for customizing rendering
destroy()Clean up resources and event listeners

Page Loader

For simpler cases where you don't need virtualization, you can create a page loader:

const pageLoader = listManager.createPageLoader(list, {
  onLoad: ({ loading, hasNext, hasPrev, items }) => {
    updateUI({ loading, hasNext, hasPrev });
  },
  pageSize: 20
});

// Initial load
await pageLoader.load();

// Navigation
nextButton.onclick = () => pageLoader.loadNext();
prevButton.onclick = () => pageLoader.loadPrev();

Route Adapter API

Creating a Route Adapter

import { createRouteAdapter } from 'core/collection';

const adapter = createRouteAdapter({
  base: 'https://api.example.com/api',
  endpoints: {
    list: '/users',
    create: '/users',
    update: '/users',
    delete: '/users'
  },
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + token
  },
  cache: true,
  onError: (error, context) => {
    console.error('API error:', error, context);
    notifyUser(`Error: ${error.message}`);
  }
});

Route Adapter Methods

MethodDescription
create(items)Create new items, returns response
read(query, options)Read items with query parameters, returns response
update(items)Update existing items, returns response
delete(ids)Delete items by ID, returns response
query(query, options)Advanced query with more options, returns response
disconnect()Clean up resources and abort pending requests

Query Operators

The route adapter supports various query operators that get transformed into appropriate URL parameters:

OperatorDescriptionExample
EQEquals{ name: { EQ: 'John' } }
NENot equals{ status: { NE: 'inactive' } }
GTGreater than{ age: { GT: 18 } }
GTEGreater than or equal{ price: { GTE: 10 } }
LTLess than{ stock: { LT: 5 } }
LTELess than or equal{ priority: { LTE: 3 } }
INIn array{ category: { IN: ['book', 'dvd'] } }
NINNot in array{ status: { NIN: ['deleted', 'archived'] } }
CONTAINSString contains{ name: { CONTAINS: 'john' } }
STARTS_WITHString starts with{ email: { STARTS_WITH: 'admin' } }
ENDS_WITHString ends with{ domain: { ENDS_WITH: '.com' } }

Usage Patterns

Basic Usage

The most common pattern is to create a Collection for your data type, then use it to manage your items with excellent performance:

// Create a Collection for your data
const tasks = createCollection<Task>();

// Add items with batch operation
await tasks.add([
  { id: '1', title: 'Complete documentation', completed: false },
  { id: '2', title: 'Write tests', completed: true }
]);

// Filter for incomplete tasks - results will be cached
tasks.query(task => !task.completed);

// Sort by title - sort results are also cached
tasks.sort((a, b) => a.title.localeCompare(b.title));

// Subscribe to changes with efficient notifications
const unsubscribe = tasks.subscribe(({ event, data }) => {
  if (event === 'change') {
    updateTaskList(data);
  }
});

// Clean up when done
unsubscribe();

API Integration

For API-connected collections, use the Route Adapter with a Collection:

// Create an adapter
const adapter = createRouteAdapter({
  base: 'https://api.example.com/api',
  endpoints: {
    list: '/products',
    create: '/products',
    update: '/products/:id',
    delete: '/products/:id'
  }
});

// Create a collection
const products = createCollection<Product>({
  transform: data => ({
    id: data._id,
    name: data.name,
    price: data.price,
    stock: data.inventory?.count || 0
  })
});

// Load data from API
const response = await adapter.read({ 
  category: 'electronics',
  price: { GTE: 100 }
}, {
  sort: 'price:desc',
  limit: 50
});

// Add items to collection
await products.add(response.items);

Virtual Lists

For handling large datasets with performance, use the List Manager:

// Create a virtual list manager
const productList = createListManager('products', containerElement, {
  renderItem: (product, index) => {
    const el = document.createElement('div');
    el.className = 'product-item';
    el.innerHTML = `
      <h3>${product.name}</h3>
      <p>${product.price.toFixed(2)}</p>
      <p>In stock: ${product.stock}</p>
    `;
    return el;
  },
  baseUrl: 'https://api.example.com/api',
  itemHeight: 80,
  pageSize: 50,
  scrollStrategy: 'hybrid'
});

// Handle out-of-stock products differently
productList.setRenderHook((product, element) => {
  if (product.stock <= 0) {
    element.classList.add('out-of-stock');
  }
});

// Scroll to a specific product
searchButton.addEventListener('click', () => {
  const productId = searchInput.value;
  productList.scrollToItem(productId, 'center');
});

// Clean up when component unmounts
onDestroy(() => {
  productList.destroy();
});

Custom Transform Functions

The module provides built-in transform functions for common data types, but you can create your own:

import { createListManager, transforms } from 'core/collection';

// Use a built-in transform
const trackList = createListManager('tracks', container, {
  transform: transforms.track,
  // ...other config
});

// Create a custom transform
const customTransform = (data) => ({
  id: data.uid || data._id,
  headline: data.title || data.name,
  supportingText: `${data.artist} • ${data.album}`,
  meta: data.duration ? formatDuration(data.duration) : '',
  original: data  // Keep original data if needed
});

const musicList = createListManager('music', container, {
  transform: customTransform,
  // ...other config
});

Examples

Creating a Collection

import { createCollection } from 'core/collection';

interface Task {
  id: string;
  title: string;
  completed: boolean;
  priority: number;
  dueDate?: string;
}

// Create a new collection with performance options
const taskCollection = createCollection<Task>({
  // Transform function to normalize data
  transform: (data) => ({
    id: data.id || `task-${Date.now()}`,
    title: data.title || 'Untitled Task',
    completed: Boolean(data.completed),
    priority: data.priority || 0,
    dueDate: data.dueDate
  }),
  
  // Validate items before adding
  validate: (item) => {
    return Boolean(item.title && item.title.trim().length > 0);
  },
  
  // Optional size hint for large collections
  initialCapacity: 500
});

// Subscribe to collection changes
const unsubscribe = taskCollection.subscribe(({ event, data }) => {
  console.log(`Collection event: ${event}`, data);
  
  if (event === 'change') {
    renderTaskList(data);
  } else if (event === 'error') {
    showErrorMessage(data.message);
  } else if (event === 'loading') {
    updateLoadingIndicator(data);
  }
});

// Add items to the collection
async function addTasks(newTasks) {
  try {
    await taskCollection.add(newTasks);
    showSuccessMessage('Tasks added successfully');
  } catch (error) {
    showErrorMessage('Failed to add tasks: ' + error.message);
  }
}

// Filter for high priority incomplete tasks
taskCollection.query(task => !task.completed && task.priority >= 3);

// Sort by due date, then by priority
taskCollection.sort((a, b) => {
  // Sort by due date first
  if (a.dueDate && b.dueDate) {
    return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime();
  } else if (a.dueDate) {
    return -1; // a has due date, b doesn't
  } else if (b.dueDate) {
    return 1;  // b has due date, a doesn't
  }
  
  // If no due dates or they're equal, sort by priority (descending)
  return b.priority - a.priority;
});

// Clean up when done
function cleanup() {
  unsubscribe();
  taskCollection.clear();
}

Setting up a List Manager

import { createListManager } from 'core/collection';

// Get container element
const container = document.getElementById('task-list-container');

// Create a list manager
const taskList = createListManager('tasks', container, {
  // Required: Function to render each item
  renderItem: (task, index, recycledElement) => {
    // Reuse element if available
    const element = recycledElement || document.createElement('div');
    element.className = 'task-item';
    element.innerHTML = `
      <div class="task-header">
        <h3>${task.title}</h3>
        <span class="priority priority-${task.priority}">P${task.priority}</span>
      </div>
      <div class="task-details">
        ${task.dueDate ? `<span class="due-date">Due: ${formatDate(task.dueDate)}</span>` : ''}
        <span class="status ${task.completed ? 'completed' : 'pending'}">
          ${task.completed ? 'Completed' : 'Pending'}
        </span>
      </div>
    `;
    
    // Add data attributes for better selection targeting
    element.dataset.id = task.id;
    element.dataset.itemType = 'task';
    
    return element;
  },
  
  // Optional: Transform function for data
  transform: (data) => ({
    id: data.id || `task-${Date.now()}`,
    title: data.title || 'Untitled Task',
    completed: Boolean(data.completed),
    priority: data.priority || 0,
    dueDate: data.dueDate
  }),
  
  // Specify API endpoint or use null for static data
  baseUrl: 'https://api.example.com/api',
  
  // Item measurements and rendering
  itemHeight: 80,
  renderBufferSize: 5,
  overscanCount: 2,
  
  // Pagination and loading
  pageSize: 25,
  loadThreshold: 0.7,
  
  // Performance options
  throttleMs: 16,
  dedupeItems: true,
  scrollStrategy: 'hybrid',
  
  // Callback after loading data
  afterLoad: ({ loading, hasNext, hasPrev, items }) => {
    // Update UI based on load status
    document.getElementById('loading-indicator').style.display = loading ? 'block' : 'none';
    document.getElementById('load-more-button').disabled = !hasNext;
    document.getElementById('item-count').textContent = `${items.length} items loaded`;
  }
});

// Add custom rendering hook
taskList.setRenderHook((task, element) => {
  // Add special styling for overdue tasks
  if (task.dueDate && new Date(task.dueDate) < new Date() && !task.completed) {
    element.classList.add('overdue');
  } else {
    element.classList.remove('overdue');
  }
  
  // Handle task completion
  const statusEl = element.querySelector('.status');
  if (statusEl) {
    statusEl.addEventListener('click', (e) => {
      e.stopPropagation();
      toggleTaskCompletion(task.id);
    });
  }
});

// Set custom heights for some items
taskList.setItemHeights({
  'task-123': 120,  // Task with notes needs more space
  'task-456': 60    // Simple task needs less space
});

// Implement search by scrolling to matching item
function searchTasks(query) {
  const tasks = taskList.getAllItems();
  const matchingTask = tasks.find(task => 
    task.title.toLowerCase().includes(query.toLowerCase())
  );
  
  if (matchingTask) {
    taskList.scrollToItem(matchingTask.id, 'center');
    highlightTask(matchingTask.id);
  }
}

// Clean up when component unmounts
function destroyTaskList() {
  taskList.destroy();
}

Working with the Route Adapter

import { createRouteAdapter, OPERATORS } from 'core/collection';

// Create a route adapter
const usersAdapter = createRouteAdapter({
  base: 'https://api.example.com/api',
  endpoints: {
    list: '/users',
    create: '/users',
    update: '/users/:id',
    delete: '/users/:id'
  },
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${getAuthToken()}`
  },
  cache: true,
  onError: (error, context) => {
    logApiError(error, context);
    showErrorNotification(`API Error: ${error.message}`);
  }
});

// Read users with filtering
async function getUsersByRole(role, status, options = {}) {
  try {
    const response = await usersAdapter.read({
      role: role,
      status: { [OPERATORS.NE]: 'deleted' },
      lastLogin: { [OPERATORS.GTE]: '2023-01-01' }
    }, {
      sort: 'name:asc',
      limit: options.limit || 50,
      fields: 'id,name,email,role,status'
    });
    
    return response.items;
  } catch (error) {
    console.error('Error fetching users:', error);
    return [];
  }
}

// Create a new user
async function createUser(userData) {
  try {
    const response = await usersAdapter.create([userData]);
    return response.items[0];
  } catch (error) {
    throw new Error(`Failed to create user: ${error.message}`);
  }
}

// Update a user
async function updateUser(id, updates) {
  // First fetch current user data
  const currentUser = await usersAdapter.read({ id: { [OPERATORS.EQ]: id } });
  
  if (!currentUser.items.length) {
    throw new Error(`User with ID ${id} not found`);
  }
  
  // Merge updates with current data
  const updatedUser = {
    ...currentUser.items[0],
    ...updates,
    id // Ensure ID is preserved
  };
  
  // Perform update
  const result = await usersAdapter.update([updatedUser]);
  return result.items[0];
}

// Delete users
async function deleteUsers(userIds) {
  try {
    const result = await usersAdapter.delete(userIds);
    return result;
  } catch (error) {
    throw new Error(`Failed to delete users: ${error.message}`);
  }
}

// Advanced query example
async function searchUsers(criteria) {
  const query = {};
  
  // Build query conditions
  if (criteria.name) {
    query.name = { [OPERATORS.CONTAINS]: criteria.name };
  }
  
  if (criteria.roles && criteria.roles.length) {
    query.role = { [OPERATORS.IN]: criteria.roles };
  }
  
  if (criteria.status) {
    query.status = { [OPERATORS.EQ]: criteria.status };
  }
  
  if (criteria.dateRange) {
    query.createdAt = {
      [OPERATORS.GTE]: criteria.dateRange.start,
      [OPERATORS.LTE]: criteria.dateRange.end
    };
  }
  
  return usersAdapter.query(query, {
    sort: criteria.sort || 'createdAt:desc',
    limit: criteria.limit || 25,
    page: criteria.page || 1
  });
}

// Cleanup when done
function cleanup() {
  usersAdapter.disconnect();
}

Virtual Scrolling Implementation

import { createListManager } from 'core/collection';

// Get container element
const container = document.querySelector('.product-grid');

// Create styles for items
const styleSheet = document.createElement('style');
styleSheet.textContent = `
  .product-item {
    border: 1px solid #eee;
    border-radius: 8px;
    padding: 16px;
    margin: 8px;
    background: white;
    box-shadow: 0 2px 4px rgba(0,0,0,0.05);
    transition: transform 0.2s;
  }
  
  .product-item:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 8px rgba(0,0,0,0.1);
  }
  
  .product-item img {
    width: 100%;
    height: 120px;
    object-fit: contain;
    margin-bottom: 8px;
  }
  
  .product-name {
    font-weight: bold;
    margin-bottom: 4px;
  }
  
  .product-price {
    color: #e63946;
    font-weight: bold;
  }
  
  .product-stock {
    font-size: 0.8em;
    color: #666;
  }
  
  .out-of-stock {
    opacity: 0.7;
  }
  
  .out-of-stock .product-stock {
    color: #e63946;
  }
`;
document.head.appendChild(styleSheet);

// Create the list manager
const productGrid = createListManager('products', container, {
  // Item rendering function
  renderItem: (product, index, recycledElement) => {
    // Either use the recycled element or create a new one
    const element = recycledElement || document.createElement('div');
    element.className = 'product-item';
    
    // For dynamic height calculation, consider image height variations
    if (product.image) {
      element.dataset.needsMeasurement = 'true';
    }
    
    element.innerHTML = `
      ${product.image ? `<img src="${product.image}" alt="${product.name}">` : ''}
      <div class="product-name">${product.name}</div>
      <div class="product-price">${product.price.toFixed(2)}</div>
      <div class="product-stock">
        ${product.stock > 0 ? `${product.stock} in stock` : 'Out of stock'}
      </div>
    `;
    
    return element;
  },
  
  // Transform function for data normalization
  transform: (data) => ({
    id: data.id || data._id,
    name: data.name || 'Unnamed Product',
    price: typeof data.price === 'number' ? data.price : parseFloat(data.price || '0'),
    stock: data.stock || data.inventory || 0,
    image: data.image || data.imageUrl || null
  }),
  
  // API connection
  baseUrl: 'https://api.example.com/api',
  
  // Layout and item sizing
  itemHeight: 220, // Base height, will be adjusted after measurement
  
  // Scroll and performance settings
  scrollStrategy: 'hybrid', // Use both scroll events and intersection observer
  dedupeItems: true,        // Prevent duplicate items
  loadThreshold: 0.7,       // Load more when 70% scrolled
  
  // Callback after data loads
  afterLoad: ({ loading, hasNext }) => {
    updateLoadingIndicator(loading);
    updateLoadMoreButton(hasNext);
  }
});

// Add a render hook for special cases
productGrid.setRenderHook((product, element) => {
  // Add out-of-stock class for styling
  if (product.stock <= 0) {
    element.classList.add('out-of-stock');
  } else {
    element.classList.remove('out-of-stock');
  }
  
  // Add click event handler
  element.addEventListener('click', () => {
    showProductDetails(product);
  });
});

// Add filter controls
document.getElementById('filter-form').addEventListener('submit', async (e) => {
  e.preventDefault();
  
  // Get filter values
  const minPrice = parseFloat(document.getElementById('min-price').value || '0');
  const maxPrice = parseFloat(document.getElementById('max-price').value || '1000000');
  const category = document.getElementById('category').value;
  const inStock = document.getElementById('in-stock').checked;
  
  // Reload with filters
  await productGrid.loadItems({
    price: {
      GTE: minPrice,
      LTE: maxPrice
    },
    category: category || undefined,
    stock: inStock ? { GT: 0 } : undefined
  });
});

// Add sort controls
document.getElementById('sort-select').addEventListener('change', (e) => {
  const sortValue = e.target.value;
  let sort = '';
  
  switch (sortValue) {
    case 'price-asc':
      sort = 'price:asc';
      break;
    case 'price-desc':
      sort = 'price:desc';
      break;
    case 'name-asc':
      sort = 'name:asc';
      break;
    case 'popularity':
      sort = 'popularity:desc';
      break;
  }
  
  // Reload with new sort
  productGrid.loadItems({ sort });
});

// Add search function
document.getElementById('search-form').addEventListener('submit', (e) => {
  e.preventDefault();
  const query = document.getElementById('search-input').value.trim();
  
  if (query) {
    productGrid.loadItems({ search: query });
  }
});

// Clean up on unmount
function unmountProductGrid() {
  productGrid.destroy();
  document.head.removeChild(styleSheet);
}

Performance Optimization

The Collection module is designed with performance as a primary concern. Here are the key optimization strategies used:

Result Caching

The createCollection factory implements smart caching of filtered and sorted results:

// Filtering and sorting operations cache their results
const largeCollection = createCollection<Item>();

// Initial filtering - computes and caches the result
largeCollection.query(item => item.category === 'electronics');

// Subsequent calls to getItems() use the cached result without recomputing
const items1 = largeCollection.getItems(); // Uses cached result
const items2 = largeCollection.getItems(); // Uses same cached result

// Cache is automatically invalidated when data changes
await largeCollection.add(newItem);

// This will trigger a new computation and caching
const refreshedItems = largeCollection.getItems();

Memory Efficiency

The implementation minimizes object creation and garbage collection:

// Notification system reuses objects
const collection = createCollection<DataItem>();

collection.subscribe(event => {
  // The event object is reused across notifications
  console.log(event.type); 
});

// Pre-allocate arrays where possible
await collection.add(items); // Uses optimized batch processing

// Efficient looping instead of multiple array transformations
const filtered = collection.getItems(); // Uses direct loops for large datasets

Processing Large Datasets

Special optimizations are applied for large collections:

// Create a collection with initial capacity hint
const bigCollection = createCollection<Entry>({
  initialCapacity: 10000 // Hint for expected size
});

// Large dataset operations use optimized algorithms
bigCollection.query(item => {
  // For collections >1000 items, uses direct loops instead of Array.filter()
  return item.value > 1000;
});

// Batch processing is used for multiple items
await bigCollection.add(hugeArrayOfItems); 

// Minimal object creation during event notification
bigCollection.subscribe(({ event, data }) => {
  // Event object is reused, data is passed by reference
});

These optimizations ensure the collection module maintains excellent performance even when dealing with large datasets or high-frequency operations.# Collection Module Documentation

The Collection module provides tools for managing, transforming and displaying collections of data in a performant and type-safe way. This module is designed to solve common data management patterns with highly optimized tools to handle data fetching, pagination, filtering, and virtualized rendering.