Skip to content

The SignalArray provides an elegant balance between simplicity and power, making array reactivity as natural as working with regular JavaScript arrays while providing the reactive benefits developers expect from modern frameworks.

Notifications You must be signed in to change notification settings

catpea/signal-array

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 

Repository files navigation

signal-array

The SignalArray provides an elegant balance between simplicity and power, making array reactivity as natural as working with regular JavaScript arrays while providing the reactive benefits developers expect from modern frameworks.

  • Fine-grained tracking - Tracks individual indices and properties
  • Deep reactivity - Nested objects and arrays are automatically reactive
  • Mutation tracking - Intercepts array methods and provides detailed change information
  • Computed values - Supports derived arrays that update automatically
  • Batched updates - Option to batch multiple changes into single notifications
  • Performance optimized - Only notifies relevant subscribers

Key Features:

  • Proxy-based for transparent array access
  • Method interception for push, pop, splice, etc.
  • Deep object reactivity for nested data
  • Computed/derived arrays that stay in sync
  • Flexible subscription model with detailed change info
  • Minimal API surface while being powerful

USAGE EXAMPLES

import { SignalArray } from 'signal-array';

// Basic usage

const todos = new SignalArray([
  { id: 1, text: 'Learn reactive patterns', done: false },
  { id: 2, text: 'Build SignalArray', done: false }
]);

// Subscribe to changes
const unsubscribe = todos.subscribe(({ mutations }) => {
  console.log('Todos changed:', mutations);
});

// Deep reactivity
todos[0].done = true; // Triggers update
todos.push({ id: 3, text: 'Ship to production', done: false }); // Also triggers

// Computed values
const completedCount = todos.computed(() =>
  todos.filter(todo => todo.done).length
);

const pendingTodos = todos.derivedFilter(todo => !todo.done);

BATCH UPDATES

const batchedArray = new SignalArray([], { batchUpdates: true });
batchedArray.push(1);
batchedArray.push(2);
batchedArray.push(3); // Only one update notification

COMPUTED VALUES: Advanced Computed Value Examples

import { SignalArray } from 'signal-array';

// Sample data: Shopping cart
const cart = new SignalArray([
  { id: 1, name: 'Laptop', price: 999.99, quantity: 1, category: 'electronics' },
  { id: 2, name: 'Mouse', price: 29.99, quantity: 2, category: 'electronics' },
  { id: 3, name: 'Coffee', price: 12.99, quantity: 3, category: 'food' },
  { id: 4, name: 'Notebook', price: 4.99, quantity: 5, category: 'stationery' }
]);

// Computed: Total price
const totalPrice = cart.computed(() => {
  return cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
});

console.log('Total Price:', totalPrice.get()); // Total Price: 1116.91

// Computed: Total items count
const totalItems = cart.computed(() => {
  return cart.reduce((sum, item) => sum + item.quantity, 0);
});

console.log('Total Items:', totalItems.get()); // Total Items: 11

// Computed: Average price per item
const averagePrice = cart.computed(() => {
  const total = totalPrice.get();
  const count = totalItems.get();
  return count > 0 ? total / count : 0;
});

console.log('Average Price:', averagePrice.get()); // Average Price: 101.54

FILTERED COMPUTED VALUES

// Computed: Items by category
const electronicsItems = cart.computed(() => {
  return cart.filter(item => item.category === 'electronics');
});

const foodItems = cart.computed(() => {
  return cart.filter(item => item.category === 'food');
});

// Computed: Expensive items (> $50)
const expensiveItems = cart.computed(() => {
  return cart.filter(item => item.price > 50);
});

// Computed: Items running low (quantity < 3)
const lowStockItems = cart.computed(() => {
  return cart.filter(item => item.quantity < 3);
});

GROUPED COMPUTED VALUES

// Computed: Group items by category
const itemsByCategory = cart.computed(() => {
  return cart.reduce((groups, item) => {
    const category = item.category;
    if (!groups[category]) {
      groups[category] = [];
    }
    groups[category].push(item);
    return groups;
  }, {});
});

console.log('Items by Category:', itemsByCategory.get());
// { electronics: [...], food: [...], stationery: [...] }

// Computed: Category totals
const categoryTotals = cart.computed(() => {
  const groups = itemsByCategory.get();
  const totals = {};

  for (const [category, items] of Object.entries(groups)) {
    totals[category] = items.reduce((sum, item) =>
      sum + (item.price * item.quantity), 0
    );
  }

  return totals;
});

console.log('Category Totals:', categoryTotals.get());
// { electronics: 1089.97, food: 38.97, stationery: 24.95 }

STATISTICAL COMPUTED VALUES

// Computed: Price statistics
const priceStats = cart.computed(() => {
  const prices = cart.map(item => item.price);
  const sorted = [...prices].sort((a, b) => a - b);
  const len = sorted.length;

  return {
    min: Math.min(...prices),
    max: Math.max(...prices),
    median: len % 2 === 0
      ? (sorted[len/2 - 1] + sorted[len/2]) / 2
      : sorted[Math.floor(len/2)],
    mean: prices.reduce((a, b) => a + b, 0) / len,
    total: prices.reduce((a, b) => a + b, 0)
  };
});

console.log('Price Statistics:', priceStats.get());

// Computed: Quantity distribution
const quantityDistribution = cart.computed(() => {
  const distribution = {};

  cart.forEach(item => {
    const range = item.quantity < 3 ? 'low' :
                  item.quantity < 5 ? 'medium' : 'high';
    distribution[range] = (distribution[range] || 0) + 1;
  });

  return distribution;
});

DERIVED ARRAYS WITH TRANSFORMATIONS

// Derived: Items with calculated total price
const itemsWithTotal = cart.derivedMap(item => ({
  ...item,
  total: item.price * item.quantity,
  formattedPrice: `$${item.price.toFixed(2)}`,
  formattedTotal: `$${(item.price * item.quantity).toFixed(2)}`
}));

// Derived: Sorted by total price (descending)
const sortedByValue = new SignalArray([]);
cart.subscribe(() => {
  const sorted = [...cart.data].sort((a, b) =>
    (b.price * b.quantity) - (a.price * a.quantity)
  );
  sortedByValue.data = sorted;
  sortedByValue.notify('sorted', { source: 'value' });
});

COMPLEX COMPUTED CHAINS

// Computed: Discount calculation based on total
const discount = cart.computed(() => {
  const total = totalPrice.get();
  if (total > 1000) return 0.15;      // 15% off
  if (total > 500) return 0.10;       // 10% off
  if (total > 100) return 0.05;       // 5% off
  return 0;
});

// Computed: Final price after discount
const finalPrice = cart.computed(() => {
  const total = totalPrice.get();
  const discountRate = discount.get();
  return total * (1 - discountRate);
});

// Computed: Savings amount
const savings = cart.computed(() => {
  return totalPrice.get() - finalPrice.get();
});

// Computed: Order summary
const orderSummary = cart.computed(() => {
  return {
    items: totalItems.get(),
    subtotal: totalPrice.get(),
    discountPercentage: (discount.get() * 100) + '%',
    discountAmount: savings.get(),
    total: finalPrice.get(),
    formattedTotal: `$${finalPrice.get().toFixed(2)}`
  };
});

console.log('Order Summary:', orderSummary.get());

SEARCH AND FILTER COMPUTEDS

// Search functionality
let searchTerm = '';

const searchResults = cart.computed(() => {
  if (!searchTerm) return cart.data;

  const term = searchTerm.toLowerCase();
  return cart.filter(item =>
    item.name.toLowerCase().includes(term) ||
    item.category.toLowerCase().includes(term)
  );
});

// Update search
function updateSearch(term) {
  searchTerm = term;
  // Trigger recomputation
  cart.notify('search', { term });
}

VALIDATION COMPUTED VALUES

// Computed: Validation status
const validationStatus = cart.computed(() => {
  const errors = [];
  const warnings = [];

  cart.forEach((item, index) => {
    if (item.quantity <= 0) {
      errors.push(`Item at index ${index} has invalid quantity`);
    }
    if (item.price <= 0) {
      errors.push(`Item at index ${index} has invalid price`);
    }
    if (item.quantity > 100) {
      warnings.push(`Item at index ${index} has unusually high quantity`);
    }
  });

  return {
    isValid: errors.length === 0,
    errors,
    warnings,
    hasWarnings: warnings.length > 0
  };
});

TIME-BASED COMPUTED VALUES

// Track when items were added
const itemsWithTimestamp = new SignalArray([]);

// Computed: Recent items (last 5 minutes)
const recentItems = itemsWithTimestamp.computed(() => {
  const fiveMinutesAgo = Date.now() - (5 * 60 * 1000);
  return itemsWithTimestamp.filter(item =>
    item.addedAt > fiveMinutesAgo
  );
});

// Computed: Items added today
const todaysItems = itemsWithTimestamp.computed(() => {
  const today = new Date();
  today.setHours(0, 0, 0, 0);

  return itemsWithTimestamp.filter(item =>
    item.addedAt >= today.getTime()
  );
});

NESTED COMPUTED VALUES

// Example: Task management with subtasks
const tasks = new SignalArray([
  {
    id: 1,
    title: 'Project A',
    subtasks: [
      { id: 11, title: 'Design', done: true },
      { id: 12, title: 'Implementation', done: false },
      { id: 13, title: 'Testing', done: false }
    ]
  },
  {
    id: 2,
    title: 'Project B',
    subtasks: [
      { id: 21, title: 'Research', done: true },
      { id: 22, title: 'Development', done: true }
    ]
  }
]);

// Computed: Task completion status
const taskCompletionStatus = tasks.computed(() => {
  return tasks.map(task => {
    const total = task.subtasks.length;
    const completed = task.subtasks.filter(st => st.done).length;

    return {
      id: task.id,
      title: task.title,
      progress: total > 0 ? completed / total : 0,
      percentComplete: total > 0 ? Math.round((completed / total) * 100) : 0,
      isComplete: completed === total,
      remaining: total - completed
    };
  });
});

// Computed: Overall progress
const overallProgress = tasks.computed(() => {
  let totalSubtasks = 0;
  let completedSubtasks = 0;

  tasks.forEach(task => {
    totalSubtasks += task.subtasks.length;
    completedSubtasks += task.subtasks.filter(st => st.done).length;
  });

  return {
    total: totalSubtasks,
    completed: completedSubtasks,
    percentage: totalSubtasks > 0
      ? Math.round((completedSubtasks / totalSubtasks) * 100)
      : 0
  };
});

MEMOIZED COMPUTED VALUES

// Expensive computation with memoization
const expensiveComputation = (() => {
  const cache = new Map();

  return cart.computed(() => {
    // Create cache key from current state
    const cacheKey = JSON.stringify(cart.map(item => ({
      id: item.id,
      price: item.price,
      quantity: item.quantity
    })));

    // Check cache
    if (cache.has(cacheKey)) {
      console.log('Using cached result');
      return cache.get(cacheKey);
    }

    // Expensive calculation
    console.log('Computing expensive result...');
    const result = cart.reduce((acc, item) => {
      // Simulate expensive operation
      let value = 0;
      for (let i = 0; i < 1000; i++) {
        value += Math.sqrt(item.price * item.quantity);
      }
      return acc + value;
    }, 0);

    // Cache result
    cache.set(cacheKey, result);

    // Limit cache size
    if (cache.size > 10) {
      const firstKey = cache.keys().next().value;
      cache.delete(firstKey);
    }

    return result;
  });
})();

REACTIVE COMPUTED DEPENDENCIES

// External reactive values
const taxRate = { value: 0.08 }; // 8% tax
const shippingRate = { value: 9.99 }; // flat shipping

// Computed with external dependencies
const orderTotal = cart.computed(() => {
  const subtotal = totalPrice.get();
  const tax = subtotal * taxRate.value;
  const shipping = subtotal > 50 ? 0 : shippingRate.value;

  return {
    subtotal,
    tax,
    shipping,
    total: subtotal + tax + shipping,
    breakdown: {
      subtotal: `$${subtotal.toFixed(2)}`,
      tax: `$${tax.toFixed(2)}`,
      shipping: shipping === 0 ? 'FREE' : `$${shipping.toFixed(2)}`,
      total: `$${(subtotal + tax + shipping).toFixed(2)}`
    }
  };
});

COMPUTED VALUES USAGE EXAMPLES

// Example 1: Real-time updates
cart.push({ id: 5, name: 'Keyboard', price: 79.99, quantity: 1, category: 'electronics' });
console.log('Updated Total:', totalPrice.get());
console.log('Updated Summary:', orderSummary.get());

// Example 2: Modify existing item
cart[0].quantity = 2;
cart.notify('quantity', { index: 0 });
console.log('After quantity change:', totalPrice.get());

// Example 3: Batch operations
const updates = () => {
  cart[1].price = 34.99;
  cart[2].quantity = 5;
  cart.push({ id: 6, name: 'Tea', price: 8.99, quantity: 2, category: 'food' });
};

// Subscribe to see all changes
const unsubscribe = cart.subscribe(({ mutations }) => {
  console.log('Cart mutations:', mutations);
  console.log('New total:', totalPrice.get());
  console.log('Category totals:', categoryTotals.get());
});

updates();

// Example 4: Reactive chain
console.log('Full order details:', {
  items: cart.map(item => ({
    name: item.name,
    subtotal: (item.price * item.quantity).toFixed(2)
  })),
  summary: orderSummary.get(),
  validation: validationStatus.get()
});

About

The SignalArray provides an elegant balance between simplicity and power, making array reactivity as natural as working with regular JavaScript arrays while providing the reactive benefits developers expect from modern frameworks.

Resources

Stars

Watchers

Forks

Packages

No packages published