Package Exports
- limitedlet
Readme
๐ limitedlet
Limited mutable variables for JavaScript and TypeScript โ because sometimes you need more safety than
letbut less thanconst
๐ Quick Start
npm install limitedletimport { limitedLet } from 'limitedlet';
// Create a variable that can only be changed 3 times
const apiCalls = limitedLet(0, 3);
apiCalls.value = 1; // โ
First mutation
apiCalls.value = 2; // โ
Second mutation
apiCalls.value = 3; // โ
Third mutation
apiCalls.value = 4; // โ Throws MutationLimitExceeded๐ Table of Contents
- ๐ฏ Concept
- ๐ก At a Glance
- ๐ API Reference
- ๐ง TypeScript Support
- ๐ Real-World Examples
- โฐ Variable History & Time Travel
- โ๏ธ Configuration
- ๐๏ธ Advanced Patterns
- ๐งช Testing Patterns
- โก Performance
๐ฏ Concept
Think of limitedlet as the missing piece between const and let:
| Type | Mutations Allowed | Use Case |
|---|---|---|
const |
0 | Truly immutable values |
limitedlet |
n | Controlled mutability |
let |
โ | Unrestricted changes |
Perfect for scenarios where you need:
- Controlled state changes (configuration updates)
- Usage quotas (API calls, retries, power-ups)
- Behavioral tracking (user interaction patterns)
- Gradual migrations (feature rollouts with safety nets)
๐ก At a Glance
import { limitedLet } from 'limitedlet';
// ๐ฏ Basic usage - single mutation allowed (default)
const setting = limitedLet('initial');
setting.value = 'updated'; // โ
setting.value = 'again'; // โ Error
// ๐ข Multiple mutations with tracking
const counter = limitedLet(0, 5, {
onMutate: ({ oldValue, newValue, remaining }) => {
console.log(`${oldValue} โ ${newValue}, ${remaining} left`);
}
});
// ๐ Non-strict mode for behavioral analysis
const tracker = limitedLet('data', 2, {
strictMode: false, // Don't throw errors, just track
onLimitExceeded: (attempt) => {
analytics.track('quota_exceeded', {
value: attempt.attemptedValue,
attempt: attempt.attemptNumber
});
}
});
// ๐ฎ Complex objects and arrays
const gameState = limitedLet({ level: 1, lives: 3 }, 10);
const inventory = limitedLet(['sword'], 5);๐ API Reference
limitedLet(initialValue, maxMutations?, options?)
Creates a limited mutable variable with powerful tracking and control features.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
initialValue |
T |
- | Initial value of any type |
maxMutations |
number |
1 |
Maximum allowed mutations |
options |
LimitedLetOptions<T> |
{} |
Configuration object |
Options
interface LimitedLetOptions<T> {
trackHistory?: boolean; // Track all mutations (default: true)
strictMode?: boolean; // Throw errors on violations (default: true)
allowReset?: boolean; // Allow resetting mutation count (default: false)
autoFreeze?: boolean; // Auto-freeze after last mutation (default: true)
onMutate?: (event: MutationEvent<T>) => void; // Called on each mutation
onLastMutation?: (event: LastMutationEvent<T>) => void; // Called on final mutation
onViolation?: (error: MutationLimitExceeded) => void; // Called on first violation
onLimitExceeded?: (attempt: ViolationAttempt<T>) => void; // Called on each violation attempt
errorMessage?: string; // Custom error message
}Properties & Methods
// Properties (read-only)
variable.value // Current value (get/set)
variable.remaining // Mutations remaining
variable.mutationCount // Successful mutations made
variable.violationCount // Violation attempts (non-strict mode)
variable.maxMutations // Maximum allowed mutations
variable.history // Array of all changes (if tracking enabled)
// Methods
variable.isDepleted() // true if all mutations used
variable.isFrozen() // true if manually or auto-frozen
variable.freeze() // Manually freeze variable
variable.reset() // Reset mutation counter (if allowed)
variable.toString() // String representation
variable.toJSON() // JSON serialization๐ง TypeScript Support
First-class TypeScript support with full generic inference:
// โจ Basic typed variables
const count = limitedLet<number>(0, 5);
const message = limitedLet<string>('hello', 3);
const flag = limitedLet<boolean>(true, 2);
// ๐๏ธ Complex interfaces
interface UserConfig {
theme: 'light' | 'dark';
language: string;
notifications: boolean;
}
const config = limitedLet<UserConfig>({
theme: 'light',
language: 'en',
notifications: true
}, 3);
// ๐ฏ Type-safe callbacks
const tracker = limitedLet<number>(0, 5, {
onMutate: (event: MutationEvent<number>) => {
// event.oldValue and event.newValue are typed as number
console.log(`Changed from ${event.oldValue} to ${event.newValue}`);
},
onLimitExceeded: (attempt: ViolationAttempt<number>) => {
// attempt.attemptedValue is typed as number
logAnalytics('limit_exceeded', attempt.attemptedValue);
}
});
// ๐ Union types
type Status = 'idle' | 'loading' | 'success' | 'error';
const status = limitedLet<Status>('idle', 3);
// ๐ฆ Array types
const items = limitedLet<string[]>(['initial'], 10);
items.value = ['updated', 'array'];
// ๐๏ธ React integration
import { useState, useRef } from 'react';
function useLimitedState<T>(
initialValue: T,
maxMutations: number,
options?: LimitedLetOptions<T>
) {
const limitedRef = useRef<LimitedVariableProxy<T>>();
const [, forceUpdate] = useState({});
if (!limitedRef.current) {
limitedRef.current = limitedLet(initialValue, maxMutations, {
...options,
onMutate: () => forceUpdate({})
});
}
return limitedRef.current;
}๐ Real-World Examples
๐ URL Redirects
Limit login attempts, then redirect to help:
const loginAttempts = limitedLet(0, 3, {
onLastMutation: () => {
showMessage('Final login attempt - please be careful!');
},
onLimitExceeded: () => {
// Redirect to help after 3 failed attempts
window.location.href = '/forgot-password';
}
});
async function handleLogin(credentials) {
try {
await authenticateUser(credentials);
window.location.href = '/dashboard';
} catch (error) {
loginAttempts.value += 1;
showError(`Login failed. ${loginAttempts.remaining} attempts remaining.`);
}
}๐ฌ Modal Dialogs
Show helpful hints, then disable after overuse:
const hintDialogs = limitedLet(0, 3, {
onMutate: ({ newValue, remaining }) => {
showHintDialog(`Tip #${newValue}: ${getTip(newValue)}`);
if (remaining === 0) {
showMessage('No more hints available. Check our documentation!');
}
},
onLimitExceeded: () => {
// Open documentation in new tab
window.open('/docs/getting-started', '_blank');
}
});
function showHint() {
if (hintDialogs.isDepleted()) {
showMessage('All hints used. Opening documentation...');
window.open('/docs', '_blank');
} else {
hintDialogs.value += 1;
}
}๐ฎ Game Mechanics
Power-ups with limited uses per level:
const powerUps = limitedLet({ shields: 3, bombs: 2, speed: 1 }, 5, {
onMutate: ({ newValue, remaining }) => {
updateUI(newValue);
if (remaining === 1) {
showWarning('One power-up modification remaining!');
}
},
onLastMutation: () => {
achievement.unlock('POWER_MANAGER');
showMessage('Power-up configuration locked for this level!');
}
});
function upgradePowerUp(type) {
if (!powerUps.isDepleted()) {
powerUps.value = {
...powerUps.value,
[type]: powerUps.value[type] + 1
};
} else {
showMessage('Power-up upgrades locked! Complete level to reset.');
}
}๐ฆ API Rate Limiting
Smart API throttling with automatic fallbacks:
const apiLimiter = limitedLet('ready', 10, {
strictMode: false, // Don't break the app
onLimitExceeded: (attempt) => {
// Switch to cached data after limit
if (attempt.attemptNumber === 1) {
showWarning('API limit reached. Using cached data.');
enableCacheMode();
}
// Open docs after multiple violations
if (attempt.attemptNumber >= 3) {
window.open('/docs/api-limits', '_blank');
}
analytics.track('api_limit_exceeded', {
endpoint: attempt.attemptedValue,
total_attempts: attempt.totalAttempts
});
}
});
async function apiCall(endpoint) {
if (!apiLimiter.isDepleted()) {
apiLimiter.value = endpoint;
return await fetch(endpoint);
} else {
// Use cached data or alternative approach
return getCachedData(endpoint);
}
}โฐ Variable History & Time Travel
If you're familiar with Redux time-traveling, this should suit you nicely!
limitedlet provides comprehensive history tracking that captures every mutation, violation, and reset with timestamps and metadata. Perfect for debugging, analytics, undo/redo systems, and behavioral analysis.
๐ฏ Basic History Tracking
const tracked = limitedLet('initial', 3, {
trackHistory: true // enabled by default
});
tracked.value = 'first';
tracked.value = 'second';
tracked.value = 'third';
console.log(tracked.history);
// [
// { value: 'initial', timestamp: 1640995200000, mutation: 0, type: 'initial' },
// { value: 'first', previousValue: 'initial', timestamp: 1640995201000, mutation: 1, type: 'mutation' },
// { value: 'second', previousValue: 'first', timestamp: 1640995202000, mutation: 2, type: 'mutation' },
// { value: 'third', previousValue: 'second', timestamp: 1640995203000, mutation: 3, type: 'mutation' }
// ]๐ Advanced History with Violations & Resets
const complex = limitedLet('start', 2, {
strictMode: false, // Track violations without throwing
allowReset: true, // Enable reset capability
trackHistory: true
});
complex.value = 'mutation1';
complex.value = 'mutation2';
complex.value = 'violation1'; // Tracked, not blocked
complex.reset();
complex.value = 'after-reset';
console.log(complex.history);
// [
// { value: 'start', timestamp: ..., mutation: 0, type: 'initial' },
// { value: 'mutation1', previousValue: 'start', mutation: 1, type: 'mutation' },
// { value: 'mutation2', previousValue: 'mutation1', mutation: 2, type: 'mutation' },
// { value: 'violation1', previousValue: 'mutation2', mutation: 2, type: 'violation' },
// { value: 'violation1', timestamp: ..., mutation: 0, type: 'reset' },
// { value: 'after-reset', previousValue: 'violation1', mutation: 1, type: 'mutation' }
// ]๐ ๏ธ Practical Applications
Time-Travel Debugging
const debugVar = limitedLet({ user: 'john', role: 'admin' }, 10, {
trackHistory: true,
onMutate: ({ newValue }) => {
console.log('๐ State changed:', newValue);
saveToDevTools(debugVar.history);
}
});
// Later in development...
function timeTravel(stepBack = 1) {
const history = debugVar.history;
const targetState = history[history.length - 1 - stepBack];
console.log('โช Time traveling to:', targetState);
return targetState.value;
}Undo/Redo System
class UndoRedoManager {
constructor(variable) {
this.variable = variable;
this.currentIndex = variable.history.length - 1;
}
undo() {
if (this.currentIndex > 0) {
this.currentIndex--;
const prevState = this.variable.history[this.currentIndex];
return prevState.value;
}
return null;
}
redo() {
if (this.currentIndex < this.variable.history.length - 1) {
this.currentIndex++;
const nextState = this.variable.history[this.currentIndex];
return nextState.value;
}
return null;
}
getTimeline() {
return this.variable.history.map((entry, index) => ({
...entry,
isCurrent: index === this.currentIndex,
relativeTime: new Date(entry.timestamp).toLocaleTimeString()
}));
}
}
const document = limitedLet('Hello', 10, { trackHistory: true });
const undoRedo = new UndoRedoManager(document);
document.value = 'Hello World';
document.value = 'Hello World!';
console.log(undoRedo.undo()); // "Hello World"
console.log(undoRedo.undo()); // "Hello"
console.log(undoRedo.redo()); // "Hello World"
console.log(undoRedo.getTimeline()); // Full timeline with timestampsUser Behavior Analytics
const userActions = limitedLet(0, 5, {
strictMode: false,
trackHistory: true,
onLimitExceeded: (attempt) => {
// Analyze user patterns from history
const patterns = analyzeUserBehavior(userActions.history);
analytics.track('user_behavior_analysis', {
session_id: getSessionId(),
total_attempts: attempt.totalAttempts,
violation_frequency: patterns.violationFrequency,
time_between_actions: patterns.averageTimeBetween,
action_sequence: patterns.actionSequence
});
}
});
function analyzeUserBehavior(history) {
const mutations = history.filter(h => h.type === 'mutation');
const violations = history.filter(h => h.type === 'violation');
return {
violationFrequency: violations.length / mutations.length,
averageTimeBetween: calculateAverageTimeBetween(history),
actionSequence: history.map(h => ({ type: h.type, value: h.value })),
peakUsageTime: findPeakUsageHours(history)
};
}React DevTools Integration
// Custom React hook with history integration
function useTrackedState(initialValue, maxMutations = 5) {
const [variable] = useState(() =>
limitedLet(initialValue, maxMutations, {
trackHistory: true,
onMutate: (event) => {
// Send to React DevTools
if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
window.__REACT_DEVTOOLS_GLOBAL_HOOK__.onCommitFiberRoot(
null,
{ memoizedState: event.newValue, history: variable.history }
);
}
}
})
);
return {
value: variable.value,
setValue: (newValue) => variable.value = newValue,
history: variable.history,
timeTravel: (index) => variable.history[index]?.value
};
}
// Usage in component
function MyComponent() {
const state = useTrackedState({ count: 0, name: 'demo' }, 8);
return (
<div>
<button onClick={() => state.setValue({...state.value, count: state.value.count + 1})}>
Increment ({state.value.count})
</button>
{/* Show history in development */}
{process.env.NODE_ENV === 'development' && (
<details>
<summary>History ({state.history.length} entries)</summary>
<pre>{JSON.stringify(state.history, null, 2)}</pre>
</details>
)}
</div>
);
}๐ History Entry Types
Each history entry contains:
interface HistoryEntry {
value: any; // The value at this point
previousValue?: any; // Previous value (for mutations/violations)
timestamp: number; // When this change occurred
mutation: number; // Current mutation count at this point
type: 'initial' | 'mutation' | 'reset' | 'violation';
}initial: The starting state when variable was createdmutation: A valid change within the mutation limitreset: When the variable was reset (ifallowReset: true)violation: An attempted change beyond the limit (ifstrictMode: false)
โก Performance Considerations
// For production: disable history if not needed
const production = limitedLet(value, 10, {
trackHistory: false // Saves memory and improves performance
});
// For development: full tracking
const development = limitedLet(value, 10, {
trackHistory: true,
onMutate: (event) => console.log('๐ Mutation:', event)
});โ๏ธ Configuration
Strict Mode vs Non-Strict Mode
Choose the right mode for your use case:
// ๐ Strict Mode (Production) - Default
const production = limitedLet('config', 3, {
strictMode: true, // Throws errors on violations
onViolation: (error) => {
logger.error('Config violation', error);
alertAdmin(error.context);
}
});
// ๐ Non-Strict Mode (Tracking/Development)
const development = limitedLet('feature', 3, {
strictMode: false, // Track violations without throwing
onLimitExceeded: (attempt) => {
analytics.track('feature_overuse', {
value: attempt.attemptedValue,
user_id: getCurrentUser().id,
timestamp: attempt.timestamp
});
}
});Semantic Coupling
limitedlet automatically handles option conflicts:
// When strictMode: false, autoFreeze is automatically disabled
const tracker = limitedLet('value', 2, {
strictMode: false, // autoFreeze becomes false automatically
onLimitExceeded: (attempt) => {
// This will continue to fire even after limit exceeded
console.log(`Violation ${attempt.attemptNumber} tracked`);
}
});
// Manual control (advanced usage)
const manual = limitedLet('value', 2, {
strictMode: true,
autoFreeze: false, // Manually disable auto-freeze
});๐๏ธ Advanced Patterns
Immutable State (Zero Mutations)
Create truly immutable variables:
const constant = limitedLet('IMMUTABLE', 0);
console.log(constant.value); // "IMMUTABLE"
constant.value = 'change'; // โ Throws immediately
// Perfect for configuration that should never change
const API_KEY = limitedLet(process.env.API_KEY, 0);Complex Object Mutations
Handle nested objects and arrays:
// Object mutations
const settings = limitedLet({ theme: 'dark', lang: 'en' }, 5);
settings.value = { ...settings.value, theme: 'light' };
// Array mutations
const tags = limitedLet(['javascript'], 3);
tags.value = [...tags.value, 'typescript'];
tags.value = tags.value.filter(tag => tag !== 'javascript');
// Nested structures
const user = limitedLet({
profile: { name: 'John', age: 30 },
preferences: { notifications: true }
}, 10);
user.value = {
...user.value,
profile: { ...user.value.profile, age: 31 }
};Type Coercion Support
Works seamlessly with JavaScript's type system:
const num = limitedLet(42, 3);
// Arithmetic operations
console.log(num + 8); // 50
console.log(num * 2); // 84
// String coercion
console.log(`Value: ${num}`); // "Value: 42"
console.log(String(num)); // "42"
// Boolean coercion
console.log(!!num); // true
// Comparison
console.log(num == 42); // true
console.log(num === 42); // false (proxy object)
console.log(num.valueOf()); // 42Resettable Variables
Enable reset capability for testing or special scenarios:
const resettable = limitedLet(0, 3, {
allowReset: true,
trackHistory: true
});
resettable.value = 1;
resettable.value = 2;
resettable.value = 3;
console.log(resettable.isDepleted()); // true
// Reset and continue
resettable.reset();
console.log(resettable.remaining); // 3
console.log(resettable.history.length); // 4 (includes reset entry)
resettable.value = 10; // โ
Works again๐งช Testing Patterns
Examples derived from our comprehensive test suite:
Basic Functionality Tests
// Test mutation limits
const counter = limitedLet(0, 3);
assert.equal(counter.remaining, 3);
counter.value = 1;
assert.equal(counter.mutationCount, 1);
assert.equal(counter.remaining, 2);
// Test error throwing
assert.throws(() => {
counter.value = 2;
counter.value = 3;
counter.value = 4; // Should throw
}, MutationLimitExceeded);
// Test callback execution
let callbackFired = false;
const tracked = limitedLet('start', 1, {
onLastMutation: () => { callbackFired = true; }
});
tracked.value = 'end';
assert.equal(callbackFired, true);Data Type Testing
// Boolean values
const bool = limitedLet(true, 2);
bool.value = false;
bool.value = true;
assert.throws(() => bool.value = false);
// Null and undefined
const nullVar = limitedLet(null, 1);
nullVar.value = undefined;
assert.equal(nullVar.value, undefined);
// Arrays
const arr = limitedLet([1, 2], 2);
arr.value = [3, 4];
arr.value = [5, 6, 7];
assert.deepEqual(arr.value, [5, 6, 7]);History and Serialization
const historical = limitedLet(10, 2, { trackHistory: true });
historical.value = 20;
historical.value = 30;
const history = historical.history;
assert.equal(history.length, 3);
assert.equal(history[0].type, 'initial');
assert.equal(history[1].type, 'mutation');
assert.equal(history[2].value, 30);
// JSON serialization
const json = historical.toJSON();
assert.equal(json.value, 30);
assert.equal(json.mutationCount, 2);
assert.equal(json.remaining, 0);โก Performance
Benchmarks
limitedlet is designed for minimal overhead:
- Memory: ~18KB compressed, zero dependencies
- CPU: Proxy overhead ~0.1ms per access
- History: Optional tracking, disabled for production if needed
Optimization Tips
// Disable history tracking for better performance
const fast = limitedLet(value, 10, {
trackHistory: false
});
// Use frozen variables to prevent further mutations
const optimized = limitedLet(data, 5);
// ... perform mutations ...
optimized.freeze(); // No more mutation checking needed
// Batch operations for better performance
const batch = limitedLet([], 1);
batch.value = items.map(transform).filter(validate); // Single mutation๐ค Contributing
We welcome contributions! Please see our Contributing Guide for details.
Development Setup
git clone https://github.com/lofimichael/limitedlet.git
cd limitedlet
npm install
npm test # Run tests
npm run demo # Run examplesInteractive Demo
Try our comprehensive React demo:
cd demo
npm install
npm run dev # Opens at http://localhost:9002The demo showcases real-world usage patterns including:
- ๐ฏ Counter with visual progress tracking
- โ๏ธ Configuration management with clean UI
- ๐ฆ API rate limiting simulation
- ๐ Behavioral tracking in non-strict mode
๐ License
MIT ยฉ limitedlet contributors
โญ Star us on GitHub โข ๐ View Docs โข ๐ฌ Join Discussion
Made with โค๏ธ for developers who need controlled mutability