How to keep GitHub Codeowners from getting removed from notifications when one team member reviews the PR

If you’re on a team that has branch protections in place that require more than one pull request (PR) approval, GitHub’s default behavior can cause notifications to be dismissed prematurely. For instance, let’s say a team is assigned to review a pull request, and one team member provides their approval. In this scenario, GitHub dismisses notifications for other team members, assuming that the pull request no longer requires additional review. However, this can be problematic if branch protections mandate multiple approvals. An effective workaround is to have the latest PR reviewer reassign the team for additional review. Yet, this manual process is prone to oversight. So, how can we automate this process?

A solution to this dilemma lies in setting up GitHub’s auto assignment feature. By enabling auto assignment, whenever your team is requested to review a PR, the system automatically removes the team as a reviewer and designates a specified subset of team members in its place. You can include your entire team in this subset to ensure everyone receives notifications without the need for manual reassignment. For detailed guidance on configuring your team’s settings for this automation, refer to the documentation provided here.

Vue 3 Composition API Lifecycle Hooks

Lately I’ve been working with Vue 3, and I figured it would be helpful to share more insight into this JavaScript framework the more I delve into it. When it comes to Vue’s lifecycle hooks, I think it’s useful to understand when and where you can use them.

Vue 3 lifecycle hooks are special functions that allow you to run code at specific moments in the life of a Vue component. Each hook corresponds to a different phase in the component’s existence, giving you the ability to perform tasks or respond to events at specific times. Let’s break down what each hook does:

onBeforeMount

For tasks that should complete just before the component is mounted, use onBeforeMount. This hook is ideal for actions like pre-fetching data or performing any operations that need to be completed before the component becomes visible.

import { onBeforeMount } from 'vue';

onBeforeMount(() => {
  // Tasks to perform just before mounting
});

onMounted

To interact with the DOM or execute operations after the component has been successfully mounted, use the onMounted lifecycle hook. This is a great time to access and manipulate DOM elements.

import { onMounted } from 'vue';

onMounted(() => {
  // Access and manipulate the DOM
});

onUpdated

When responsiveness to changes in state or props is crucial after an update, you can utilize onUpdated. This hook allows you to react dynamically to modifications in the component’s state or props, enabling you to trigger side effects or additional logic during re-renders.

import { onUpdated } from 'vue';

onUpdated(() => {
  // React to changes in state or props
});

onBeforeUnmount

For cleanup tasks and resource release before a component is unmounted, use onBeforeUnmount. This hook ensures that your component gracefully removes any resources acquired during its lifecycle.

import { onBeforeUnmount } from 'vue';

onBeforeUnmount(() => {
  // Cleanup tasks before unmounting
});

onErrorCaptured

For handling errors occurring within the component’s lifecycle, turn to onErrorCaptured. This hook allows you to catch and manage errors internally or propagate them to a higher level for comprehensive error handling.

import { onErrorCaptured } from 'vue';

onErrorCaptured((error, instance, info) => {
  // Handle errors within the component
});

JavaScript Design Patterns

Design patterns are guidelines for solving common problems in software development. By learning design patterns, you can quickly and easily communicate designs to other software engineers. Here is an overview of some of the common JavaScript design patterns:

Singleton

The Singleton Pattern ensures that a class has only one instance, and provides a global access point to it. This instance can be shared throughout an application, which makes Singletons great for managing global state.

Example:

let instance;

class Example {
  constructor() {
    if (instance) {
      throw new Error("You've already created an instance!")
    }
    this.example = example;
    instance = this;
  }

  getExample() {
    return this.example;
  }
}

Pros to using the Singleton Pattern

✅ You can potentially save a lot of memory since you don’t have to set up memory for a new instance.

Cons to using the Singleton Pattern

We no longer need to explicitly create Singletons since ES2015 Modules are Singletons by default.

The global variables are accessible throughout the code. This can lead to problems like race conditions.

When importing a module, it might not be obvious that the module is importing a Singleton which can lead to unexpected value changes within the Singleton.

Proxy

The Proxy pattern uses a Proxy to serve as an interface to control interactions to target objects.

Example:

const targetObject = {
  name: "User",
  message: "hello world"
}

const handler = {
  get(targetObject, prop, receiver) {
    return "!";
  }
}

const proxy = new Proxy(targetObject, handler);

console.log(proxy.message); // !

Pros to using the Proxy Pattern

✅ It’s easier to add functionality with specific objects (i.e. logging, debugging, notifications, validation, etc).

Cons to using the Proxy Pattern

Could cause performance issues from executing handlers on every object.

Observer

The Observer Pattern is the most commonly used design pattern in the real world. It defines a one-to-many dependency between objects so that when one object changes its state, all of its dependents are notified and updated automatically.

Example:

class Observable {
  constructor() {
    this.observers = [];
  }

  subscribe(exampleFunction) {
    this.observers.push(exampleFunction);
  }

  unsubscribe(exampleFunction) {
    this.observers = this.observers.filter((observer) => observer !== exampleFunction);
  }
 
  notify(data) {
    this.observers.forEach((observer) => observer(data));
  }
}

Pros to using the Observer Pattern

✅ Observer objects (that handle the received data) can be decoupled/coupled easily with the observable object (monitors the events).

Cons to using the Observer Pattern

Could potentially have performance issues because of the time it takes to notify all subscribers (i.e. if there are too many subscribers, if the logic becomes too complicated).

Factory

The Factory Pattern wraps a constructor for different types of objects and returns instances of the objects.

Example:

const createItem = (name, message) => ({
  createdAt: Date.now(),
  name,
  message,
  quote: `${name} said "${message}"`,
});

createItem("User", "Hello World!");

Pros to using the Factory Pattern

✅ Keeps code DRY and is handy when we need to create several objects that share the same properties.

Cons to using the Factory Pattern

Might be more memory efficient to create new instances instead of new objects.

Prototype

The Prototype Pattern creates new objects and returns objects that are initialized with values copied from the prototype. It is a helpful way to share properties among many objects of the same type.

Example:

const createUser = (name, message) => ({
  name,
  message,
  speak() {
    console.log(`${name} said "${message}"`);
  },
  walking() {
    console.log(`${name} is walking!`);
  },
});

const user1 = createUser("Jill", "Hello world!");
const user2 = createUser("Jack", "Good morning!");

Pros to using the Prototype Pattern

✅ More efficient with memory since we can access properties that aren’t defined directly on the object. This allows us to avoid duplication of properties and methods.

Cons to using the Prototype Pattern

Issues with readability – if a class is extended quite a few times, it can be hard to know where specific properties come from.

Additional Resources

patterns.dev

JavaScript Closures

JavaScript closures allow functions to remember the scope in which they were created, even when they are executed outside that scope. This ability to capture and retain the lexical scope is what makes closures a powerful feature.

How Closures Work

To understand closures, it’s essential to grasp the concept of lexical scope. Lexical scope refers to the way in which variable names are resolved in nested functions. Closures come into play when a function is defined within another function, creating a chain of lexical scopes.

When an inner function is returned from its outer function, it carries with it a reference to the entire scope chain in which it was defined. This reference allows the inner function to access variables and parameters from its outer function, even after the outer function has finished executing. This behavior is the essence of closures.

Practical Examples

Let’s explore a few practical examples to illustrate how closures work in real-world scenarios. We’ll look at scenarios such as data encapsulation, private variables, and callback functions to showcase the versatility and usefulness of closures.

  1. Data Encapsulation: Closures provide a way to encapsulate data within a function, preventing external code from directly accessing or modifying it. This promotes data integrity and reduces the likelihood of unintended side effects.
function createCounter() {
  let count = 0;

  return function() {
    count++;
    return count;
  };
}

const counter = createCounter();
console.log(counter()); // Output: 1
console.log(counter()); // Output: 2

In this example, the createCounter function returns an inner function that has access to the count variable. The returned function serves as a counter, and the count variable is protected from external manipulation.

  1. Private Variables: Closures enable the creation of private variables within a function, allowing you to hide implementation details and expose only the necessary interfaces.
function createPerson(name) {
  let privateAge = 0;

  return {
    getName: function() {
      return name;
    },
    getAge: function() {
      return privateAge;
    },
    setAge: function(newAge) {
      if (newAge >= 0) {
        privateAge = newAge;
      }
    },
  };
}

const person = createPerson("John");
console.log(person.getName()); // Output: John
console.log(person.getAge()); // Output: 0
person.setAge(25);
console.log(person.getAge()); // Output: 25

In this example, the createPerson function returns an object with methods to access and modify private data (privateAge). This encapsulation ensures that the internal state of the object remains controlled.

  1. Callback Functions: Closures are commonly used in the context of callback functions. When a function is passed as an argument to another function and is executed later, it forms a closure, retaining access to the variables in its lexical scope.
function delayMessage(message, delay) {
  setTimeout(function() {
    console.log(message);
  }, delay);
}

delayMessage("Hello, World!", 2000);

In this example, the anonymous function inside setTimeout forms a closure, allowing it to access the message variable from the outer delayMessage function even after delayMessage has finished executing.

JavaScript closures provide developers with tools for creating modular, maintainable, and efficient code. By understanding how closures work and applying them judiciously, you can elevate your JavaScript programming skills and build robust applications.

JavaScript Iterators

Iterators in JavaScript refer to objects that provide a sequential method of accessing elements within a collection. Collections include data structures like arrays, strings, maps, and sets. Iterators offer a standardized approach to traversing these collections, providing a controlled and flexible alternative to traditional loops.

Creating an Iterator

Iterators include a method, within an object, named Symbol.iterator, responsible for returning an iterator object:

const myIterable = {
  [Symbol.iterator]: function () {
    // Insert iterator logic here
  },
};

The iterator object, in turn, must include a method named next, returning an object with value and done properties. The value property signifies the current element in the iteration, while the done property is a boolean indicating whether more elements are available for iteration.

const myIterator = {
  next: function () {
    // return { value: ..., done: ... }
  },
};

Working with Iterators

Many data structures, including arrays, strings, maps, and sets, inherently implement iterators:

const myArray = [1, 2, 3, 4, 5];
const arrayIterator = myArray[Symbol.iterator]();

console.log(arrayIterator.next()); // { value: 1, done: false }
console.log(arrayIterator.next()); // { value: 2, done: false }
console.log(arrayIterator.next()); // { value: 3, done: false }
// Iteration continues until done is true

Enhancing Code Readability

One of the main benefits of iterators is the improvement of code readability. By abstracting away the intricacies of looping, iterators allow developers to concentrate on the logic within the loop, rather than managing indices or counting iterations. This results in more concise and expressive code.

const myArray = [1, 2, 3, 4, 5];

// Traditional for loop
for (let i = 0; i < myArray.length; i++) {
  console.log(myArray[i]);
}

// Using iterator
const arrayIterator = myArray[Symbol.iterator]();
let iterationResult = arrayIterator.next();

while (!iterationResult.done) {
  console.log(iterationResult.value);
  iterationResult = arrayIterator.next();
}

Use Cases for Iterators

By offering a standardized interface for iterating over elements, iterators simplify the integration of custom objects into existing code. Additionally, iterators play a fundamental role in the for...of loop introduced in ECMAScript 6. This loop streamlines the process of iterating over iterable objects, resulting in more readable and concise code.

Understanding the Event Loop in JavaScript

What is the Event Loop?

The event loop is a continuous process that enables JavaScript to execute code, handle events, and manage asynchronous tasks. Unlike synchronous languages, JavaScript is single-threaded, meaning it can only execute one operation at a time. The event loop ensures that asynchronous operations, such as fetching data or handling user input, can be managed without blocking the main thread.

Phases of the Event Loop

The event loop consists of multiple phases, each playing a specific role in handling different types of tasks. Understanding these phases is crucial for grasping how JavaScript manages its execution flow.

  1. Call Stack:
    • The call stack is where synchronous code is executed.
    • Functions are pushed onto the stack and popped off when they complete.
  2. Callback Queue:
    • Asynchronous operations, such as API calls or user interactions, are processed in the callback queue.
    • Callbacks from these operations are queued up to be executed once the call stack is empty.
  3. Event Loop:
    • The event loop constantly checks if the call stack is empty.
    • If the stack is empty, it moves callbacks from the queue to the stack for execution.
  4. Microtask Queue:
    • Microtasks are high-priority tasks that are executed before the next event loop cycle.
    • Promises and certain APIs schedule tasks in the microtask queue.

How the Event Loop Handles Asynchronous Operations

Let’s take a look at how the event loop manages asynchronous tasks:

  1. setTimeout:
    • setTimeout allows you to schedule a function to run after a specified delay.
    • The specified function is added to the callback queue after the delay.
  2. Promises:
    • Promises are a powerful tool for handling asynchronous operations.
    • They use the microtask queue, ensuring their callbacks are executed before the next event loop cycle.
  3. Async/Await:
    • Async/Await, built on top of Promises, provides a more readable way to work with asynchronous code.
    • Under the hood, it still relies on the event loop and the microtask queue.

What are Symlinks?

Symlinks, short for symbolic links, are a powerful and versatile feature in the world of computing. Despite being an important piece of file systems, many users are unfamiliar with what symlinks are and how they can be utilized. Let’s take a look at what they are, how they work, and how they’re beneficial.

Overview of Symlinks

At its core, a symlink is a pointer to another file or directory. Unlike a hard link, which points directly to the data blocks of a file, a symlink acts as a reference to the target file or directory. This reference is essentially a path that allows users to access the target file or directory indirectly.

How Symlinks Work

Symlinks work by storing the path to the target file or directory. When a user accesses the symlink, the operating system transparently redirects the request to the actual file or directory specified by the symlink. This provides a level of abstraction and flexibility, allowing users to create symbolic links across different file systems and even on remote servers.

Benefits of Symlinks

  1. Space Efficiency: Symlinks help save disk space by creating references to files instead of duplicating them. This is particularly useful when dealing with large datasets or when multiple instances of the same file are required.
  2. Organizing Files: Symlinks allow users to organize their files in a more intuitive manner. For example, a user might create symlinks in their home directory pointing to frequently accessed files in deeper directories, making navigation more efficient.
  3. Cross-Platform Compatibility: Symlinks can be used to create cross-platform compatibility. If a file or directory needs to be accessed on both Windows and Linux systems, symlinks can be created to provide a seamless experience.
  4. Upgrading Software: Software updates often require replacing or modifying existing files. Symlinks can be used to switch between different versions of files, making the process of upgrading or downgrading software smoother and more manageable.
  5. Simplified File Maintenance: When dealing with complex directory structures, symlinks can simplify file maintenance. They allow users to create shortcuts to important files or directories, reducing the complexity of navigating through deep directory trees.

Creating Symlinks

Creating a symlink involves using the ln command in the terminal. For example, to create a symlink named link_to_file pointing to a file named target_file, the following command can be used:

ln -s /path/to/target_file link_to_file

Understanding JavaScript Promises

JavaScript, as a versatile and dynamic programming language, has evolved over the years to offer developers powerful tools for handling asynchronous operations. Promises, introduced in ECMAScript 6 (ES6), have become a fundamental part of modern JavaScript development. Let’s take a look at what JavaScript promises are, as well as focusing on the micro-task queue and callback queue, which are crucial components in the asynchronous execution model.

JavaScript Promises

Promises are objects representing the eventual completion or failure of an asynchronous operation. They provide a cleaner and more structured way to handle asynchronous code compared to traditional callback functions. A promise can be in one of three states: pending, fulfilled, or rejected.

  1. Promise States:
    • Pending: The initial state when a promise is created.
    • Fulfilled: The state when the asynchronous operation is successfully completed.
    • Rejected: The state when the asynchronous operation encounters an error or fails.
  2. Creating Promises
    • Promises can be created using the Promise constructor. This constructor takes a function (executor) as its argument, which in turn takes two parameters: resolve and reject. Developers use these functions to signal the completion or failure of the asynchronous operation.
const myPromise = new Promise((resolve, reject) => {
  // Asynchronous operation
  if (operationSuccessful) {
    resolve(result);
  } else {
    reject(error);
  }
});

Micro-Task Queue

The micro-task queue is a key component in JavaScript’s event loop mechanism, responsible for handling micro-tasks. Micro-tasks are tasks that have higher priority than regular tasks and are executed after the current script has run to completion. Promises use the micro-task queue to schedule their callbacks.

  1. Micro-Tasks:
    • Micro-tasks include promise callbacks, process.nextTick, and Object.observe.
    • Micro-tasks are executed in a single cycle of the event loop, ensuring they are processed before the next rendering.
  2. Promise Execution Order:
    • When a promise is fulfilled or rejected, its callback is added to the micro-task queue.
    • Micro-tasks are executed in order, allowing promises to be resolved before other tasks in the callback queue.

Callback Queue

The callback queue, also known as the task queue or message queue, is another integral part of the event loop. It stores tasks that are ready to be executed, allowing asynchronous operations to be handled in a non-blocking manner.

  1. Callback Execution:
    • Tasks in the callback queue are executed after the micro-tasks.
    • Regular tasks, such as event handlers and I/O operations, are added to the callback queue.
  2. Order of Execution:
    • The event loop continuously checks the micro-task queue and executes micro-tasks before moving to the callback queue.
    • This order ensures that promises are resolved before other asynchronous operations in the callback queue.

JavaScript promises have transformed the way developers handle asynchronous code, offering a cleaner and more maintainable approach compared to traditional callbacks. Understanding the micro-task queue and callback queue is crucial for mastering the intricacies of the event loop and ensuring the efficient execution of asynchronous operations in JavaScript. As you delve deeper into asynchronous programming, a solid grasp of promises and the event loop will empower you to build robust and responsive applications.

Understanding Grep

What is grep?

grep stands for Global Regular Expression Print. In simpler terms, it’s a command-line utility that searches through text using regular expressions.

The Basics

At its core, grep is a master of finding patterns. Whether you’re searching for a specific word, a line of code, or a complex pattern, grep has got your back. Let’s take a look at the basics:

grep "search_term" file.txt

This simple command will scour file.txt for instances of “search_term” and print out the matching lines.

Regular Expressions

grep truly shines with regular expressions (regex) – you can create intricate patterns to match exactly what you need. Wildcards, quantifiers, and character classes – the regex world is vast, and grep has got your covered.

grep "^\d{3}-\d{2}-\d{4}$" data.txt

In this example, we’re searching for lines in data.txt that match the pattern of a social security number. The ^ and $ anchor the regex to the beginning and end of the line, ensuring an exact match.

Recursion

Imagine you have a project with nested folders, and you want to find where a particular function is being used:

grep -r "function_name" project_folder/

The -r flag tells grep to search recursively. It will traverse through folders, unveiling every file that contains the sacred “function_name.”

Filtering

grep isn’t just about finding; it’s about filtering too. Let’s say you want to find all JavaScript files containing the word “error,” but you’re not interested in the case:

grep -i "error" *.js

The -i flag makes the search case-insensitive, ensuring you catch all variations of “error.”

Counting and Beyond

Sometimes you just want the numbers. How many lines contain your search term? grep is on it:

grep -c "search_term" file.txt

And to go even further, combine grep with other commands by piping:

cat log.txt | grep "error" | wc -l

Here, we’re counting the lines with “error” in a log file. cat displays the file, grep finds the errors, and wc -l counts the lines.

Essential Command Line Commands for Developers

The command line interface (CLI) is a powerful tool that developers can leverage to streamline their workflow and boost productivity. While graphical user interfaces (GUIs) are user-friendly, the command line offers a more efficient and flexible way to interact with your computer.

Here are some useful commands to keep in your developer toolbox:

  1. Navigating the File System:
    • cd <directory>: Change directory. Use this command to navigate between folders (add the directory path in the place of <directory>).
    • ls: List the contents of a directory. Add options like -l for a detailed list or -a to show hidden files.
    • pwd: Print working directory. Prints the file path of the directory you’re currently in.
  2. File Operations:
    • cp: Copy files or directories. Helpful for duplicating files (use cp <file> <directory> to copy a file to a directory – which can possibly overwrite files).
    • mv: Move or rename files and directories (to move files, use mv <file> <directory>, to rename a file, use mv <file-old> <file-new>).
    • rm: Remove files or directories (rm <file> for files or rm -r <directory> for directories). Exercise caution, as this command is irreversible.
    • mkdir <directory>: Create a directory (add the name of the directory in place of <directory>).
    • touch <file>: Create a file (add the file name in place of <file>).
  3. Text Manipulation:
    • cat <file>: Concatenate and display the content of files.
    • grep: Search for specific patterns in files.
    • sed: Stream editor for filtering and transforming text.
  4. File Inspection:
    • file: Determine the file type.
    • wc: Count words, lines, and characters in a file.
    • head and tail: Display the beginning or end of a file.
  5. System Information:
    • df: Display disk space usage.
    • free: Display amount of free and used memory.
    • top and htop: Show real-time system statistics.
  6. Version Control:
    • git: Essential for version control. Commands like git clone, git pull, git push, and more are crucial for collaborative development.
  7. Package Management:
    • npm or yarn (for Node.js): Manage packages and dependencies for JavaScript projects.
    • pip (for Python): Install Python packages effortlessly.
  8. Network-related Commands:
    • ping: Test the reachability of a host.
    • curl and wget: Download files from the web directly in the terminal.
  9. Process Management:
    • ps: Display information about active processes.
    • kill: Terminate a process. Use with caution.
  10. User and Permissions:
    • sudo: Execute a command with superuser privileges.
    • chown and chmod: Change ownership and permissions of files.

Mastering the command line is a key skill for developers. These essential commands empower developers to perform a wide range of tasks efficiently and are particularly valuable in server environments and automation scripts. As you become more comfortable with the command line, you’ll discover its potential for enhancing your development workflow and troubleshooting capabilities.