JavaScript 2024 (ES15)

Promise.withResolvers, Object.groupBy, and modern enhancements

🌟 What's New in JavaScript 2024?

JavaScript 2024 (ES15) brings exciting features like Promise.withResolvers(), Object.groupBy(), and enhanced regular expressions. These additions make JavaScript more powerful for modern web development.


// New in 2024: Promise.withResolvers()
const { promise, resolve, reject } = Promise.withResolvers();

// Use resolve/reject from outside the promise
setTimeout(() => resolve('Success!'), 1000);
const result = await promise;
console.log(result); // 'Success!'
                                    

Key Features of ES15

🀝

Promise.withResolvers()

Create promises with external resolvers

const { promise, resolve } = 
  Promise.withResolvers();
resolve('Done!');
πŸ“Š

Object.groupBy()

Group array elements by key

const grouped = Object.groupBy(
  users, user => user.role
);
πŸ”€

String.isWellFormed()

Check if string is well-formed Unicode

const text = "Hello πŸ‘‹";
console.log(text.isWellFormed());
// true
🎯

ArrayBuffer.resize()

Resize ArrayBuffers dynamically

const buffer = new ArrayBuffer(8);
buffer.resize(16);
console.log(buffer.byteLength);

πŸ”Ή Promise.withResolvers()

Create promises with external control over resolution:

// Traditional promise creation
const traditionalPromise = new Promise((resolve, reject) => {
    // Logic must be inside here
    setTimeout(() => resolve('Traditional way'), 1000);
});

// New way with withResolvers()
const { promise, resolve, reject } = Promise.withResolvers();

// Can resolve from anywhere in your code!
setTimeout(() => resolve('Modern way'), 1000);

// Practical example: Event-driven promise resolution
class EventManager {
    constructor() {
        this.listeners = new Map();
    }
    
    waitForEvent(eventName) {
        const { promise, resolve } = Promise.withResolvers();
        
        // Store resolver for later use
        if (!this.listeners.has(eventName)) {
            this.listeners.set(eventName, []);
        }
        this.listeners.get(eventName).push(resolve);
        
        return promise;
    }
    
    emit(eventName, data) {
        const resolvers = this.listeners.get(eventName) || [];
        resolvers.forEach(resolve => resolve(data));
        this.listeners.delete(eventName); // Clean up
    }
}

// Usage
const eventManager = new EventManager();

// Wait for user login from anywhere in your app
const loginPromise = eventManager.waitForEvent('userLogin');
console.log('Waiting for user login...');

// Simulate login happening elsewhere
setTimeout(() => {
    eventManager.emit('userLogin', { userId: 123, name: 'Alice' });
}, 2000);

loginPromise.then(user => {
    console.log('User logged in:', user.name);
});

Output:

Waiting for user login...
User logged in: Alice

πŸ”Ή Object.groupBy() and Map.groupBy()

Group array elements by a key function:

const employees = [
    { name: 'Alice', department: 'Engineering', salary: 90000 },
    { name: 'Bob', department: 'Marketing', salary: 65000 },
    { name: 'Charlie', department: 'Engineering', salary: 95000 },
    { name: 'Diana', department: 'Sales', salary: 70000 },
    { name: 'Eve', department: 'Marketing', salary: 68000 },
    { name: 'Frank', department: 'Engineering', salary: 88000 }
];

// Group by department using Object.groupBy()
const byDepartment = Object.groupBy(employees, emp => emp.department);
console.log('Grouped by department:', byDepartment);

// Group by salary range using Map.groupBy()
const bySalaryRange = Map.groupBy(employees, emp => {
    if (emp.salary < 70000) return 'Low';
    if (emp.salary < 90000) return 'Medium';
    return 'High';
});

console.log('High earners:', bySalaryRange.get('High'));

// Advanced grouping: Multiple criteria
const products = [
    { name: 'Laptop', category: 'Electronics', price: 999, inStock: true },
    { name: 'Phone', category: 'Electronics', price: 699, inStock: false },
    { name: 'Desk', category: 'Furniture', price: 299, inStock: true },
    { name: 'Chair', category: 'Furniture', price: 199, inStock: true },
    { name: 'Tablet', category: 'Electronics', price: 399, inStock: true }
];

// Group by availability and category
const groupedProducts = Object.groupBy(products, product => 
    `${product.category}-${product.inStock ? 'Available' : 'OutOfStock'}`
);

console.log('Product groups:', Object.keys(groupedProducts));
console.log('Available Electronics:', groupedProducts['Electronics-Available']);

Output:

Grouped by department: {
  Engineering: [Alice, Charlie, Frank],
  Marketing: [Bob, Eve],
  Sales: [Diana]
}
High earners: [Alice, Charlie]
Product groups: ['Electronics-Available', 'Electronics-OutOfStock', 'Furniture-Available']
Available Electronics: [Laptop, Tablet]

πŸ”Ή String Unicode Methods

Better handling of Unicode strings:

// String.isWellFormed() - check for valid Unicode
const validString = "Hello πŸ‘‹ World 🌍";
const invalidString = "Hello\uD800World"; // Lone surrogate

console.log('Valid string:', validString.isWellFormed()); // true
console.log('Invalid string:', invalidString.isWellFormed()); // false

// String.toWellFormed() - fix malformed Unicode
const fixedString = invalidString.toWellFormed();
console.log('Fixed string:', fixedString); // "HelloοΏ½World" (replacement character)

// Practical example: Safe string processing
function safeStringProcess(input) {
    if (!input.isWellFormed()) {
        console.warn('Malformed Unicode detected, fixing...');
        input = input.toWellFormed();
    }
    
    return input.toUpperCase();
}

console.log(safeStringProcess("Hello 🌟")); // "HELLO 🌟"
console.log(safeStringProcess("Bad\uD800String")); // "BADοΏ½STRING"

// Working with emojis and complex Unicode
const emojiString = "πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ Family emoji";
console.log('Emoji string well-formed:', emojiString.isWellFormed()); // true
console.log('Length:', emojiString.length); // 16 (complex emoji counts as multiple units)

// Proper emoji handling
const emojis = ['πŸ‘‹', '🌍', 'πŸš€', 'πŸ’»', 'πŸŽ‰'];
const emojiText = emojis.join(' ');
console.log('Emoji text:', emojiText);
console.log('Is well-formed:', emojiText.isWellFormed()); // true

Output:

Valid string: true
Invalid string: false
Fixed string: HelloοΏ½World
HELLO 🌟
BADοΏ½STRING
Emoji string well-formed: true
Length: 16
Emoji text: πŸ‘‹ 🌍 πŸš€ πŸ’» πŸŽ‰
Is well-formed: true

πŸ”Ή ArrayBuffer Enhancements

Dynamic resizing and transfer of ArrayBuffers:

// Resizable ArrayBuffer
const resizableBuffer = new ArrayBuffer(8, { maxByteLength: 64 });
console.log('Initial size:', resizableBuffer.byteLength); // 8
console.log('Max size:', resizableBuffer.maxByteLength); // 64
console.log('Resizable:', resizableBuffer.resizable); // true

// Resize the buffer
resizableBuffer.resize(16);
console.log('After resize:', resizableBuffer.byteLength); // 16

// Working with views on resizable buffers
const uint8View = new Uint8Array(resizableBuffer);
uint8View[0] = 42;
uint8View[8] = 100; // Now accessible after resize

console.log('Data at index 0:', uint8View[0]); // 42
console.log('Data at index 8:', uint8View[8]); // 100

// Transferable ArrayBuffer
const transferableBuffer = new ArrayBuffer(16);
const originalView = new Uint8Array(transferableBuffer);
originalView[0] = 123;

console.log('Before transfer:', originalView[0]); // 123

// Transfer ownership (simulated - actual transfer happens between workers)
const transferredBuffer = transferableBuffer.transfer();
console.log('Original buffer detached:', transferableBuffer.byteLength); // 0
console.log('Transferred buffer size:', transferredBuffer.byteLength); // 16

// Practical example: Dynamic buffer for streaming data
class StreamBuffer {
    constructor(initialSize = 1024) {
        this.buffer = new ArrayBuffer(initialSize, { maxByteLength: 1024 * 1024 });
        this.view = new Uint8Array(this.buffer);
        this.position = 0;
    }
    
    write(data) {
        // Resize if needed
        if (this.position + data.length > this.buffer.byteLength) {
            const newSize = Math.min(
                Math.max(this.buffer.byteLength * 2, this.position + data.length),
                this.buffer.maxByteLength
            );
            this.buffer.resize(newSize);
            this.view = new Uint8Array(this.buffer);
        }
        
        // Write data
        this.view.set(data, this.position);
        this.position += data.length;
    }
    
    getData() {
        return this.view.slice(0, this.position);
    }
}

// Usage
const stream = new StreamBuffer(8);
stream.write(new Uint8Array([1, 2, 3, 4]));
stream.write(new Uint8Array([5, 6, 7, 8, 9, 10])); // Triggers resize
console.log('Stream data:', Array.from(stream.getData())); // [1,2,3,4,5,6,7,8,9,10]

Output:

Initial size: 8
Max size: 64
Resizable: true
After resize: 16
Data at index 0: 42
Data at index 8: 100
Before transfer: 123
Original buffer detached: 0
Transferred buffer size: 16
Stream data: [1,2,3,4,5,6,7,8,9,10]

πŸ”Ή Real-World Applications

Combining ES15 features for practical solutions:

// Advanced data processing pipeline
class DataProcessor {
    constructor() {
        this.cache = new Map();
        this.{ promise: processingComplete, resolve: markComplete } = Promise.withResolvers();
    }
    
    async processUserData(users) {
        // Group users by role using Object.groupBy()
        const usersByRole = Object.groupBy(users, user => user.role);
        
        // Process each group
        const results = {};
        for (const [role, roleUsers] of Object.entries(usersByRole)) {
            results[role] = await this.processRole(roleUsers);
        }
        
        // Mark processing as complete
        this.markComplete(results);
        return results;
    }
    
    async processRole(users) {
        // Simulate processing with proper Unicode handling
        return users.map(user => ({
            ...user,
            displayName: user.name.isWellFormed() ? 
                user.name : user.name.toWellFormed(),
            processed: true
        }));
    }
    
    async waitForCompletion() {
        return this.processingComplete;
    }
}

// Usage example
const processor = new DataProcessor();
const userData = [
    { id: 1, name: 'Alice πŸ‘©β€πŸ’»', role: 'developer' },
    { id: 2, name: 'Bob πŸ‘¨β€πŸ’Ό', role: 'manager' },
    { id: 3, name: 'Charlie πŸ‘¨β€πŸ’»', role: 'developer' },
    { id: 4, name: 'Diana πŸ‘©β€πŸ’Ό', role: 'manager' }
];

// Process data and wait for completion
processor.processUserData(userData);
const results = await processor.waitForCompletion();
console.log('Processing complete:', Object.keys(results));

Output:

Processing complete: ['developer', 'manager']

🧠 Test Your Knowledge

What does Promise.withResolvers() return?