2.5 OPs

Introduction

Operations, often abbreviated as OPs, play a crucial role in enhancing the Deno runtime's capabilities beyond what is outlined in the ECMAScript specification. Unlike the broader functionalities enabled by OPs, the V8 engine operates within a constrained environment that strictly adheres to the guidelines of ECMAScript and nothing more. This means that basic tasks such as reading files, managing sockets, and handling timers, among others, necessitate external Application Programming Interfaces (APIs) to function.
This is precisely where OPs come into play. They bridge the gap between the limited scope of V8's sandboxed execution and the wider range of functionalities that a runtime like Deno aims to provide. Let's delve into a simple yet illustrative example to grasp the significance of OPs: extracting an environment variable in the context of Deno.
By facilitating access to system-level resources, OPs enable Deno to interact with its surrounding environment beyond the realm of ECMAScript. Imagine you want to retrieve an environment variable within your Deno application. This seemingly straightforward task involves an OP that interfaces with the underlying operating system, fetching the desired environment variable's value and making it available for your JavaScript or TypeScript code to utilize.

Types of OPs

OPs can be categorized into two main types: synchronous ops and asynchronous ops.
Synchronous ops:
  • Synchronous operations run from start to finish without interruption.
  • When a synchronous op is executed, the ongoing execution of the script is paused until the operation completes and a result is obtained.
Asynchronous ops:
  • Asynchronous operations are scheduled to produce a result in the future.
  • The results of asynchronous ops become available at a later time and are processed without blocking the main thread.
  • Unlike synchronous ops, asynchronous ops do not halt the progress of the program execution.
When it comes to synchronous ops, they halt the current thread's progress until the desired result is achieved. On the other hand, asynchronous ops allow the program to continue running without waiting for the result.
To implement asynchronous ops, Deno employs the tokio runtime, which utilizes a mechanism known as asynchronous green threads. These threads facilitate the execution of asynchronous ops without impeding the overall program flow. We'll delve deeper into the workings of tokio shortly.
It's important to note that certain operations can be both synchronous and asynchronous, depending on their nature. Meanwhile, others exclusively fall under the category of synchronous ops. The classification of an operation depends on its specific characteristics. For instance, consider the "getRandomValues()" function in the crypto module. This function is exclusively available as a synchronous op through "op_get_random_values()." Since this function involves intensive CPU computations, there's no need to provide an asynchronous version of it.

List of OPs

Let's go over the list of services provided by Ops. As always, the list is abbreviated to include only some of them:
  • Crypto
  • Fetch
  • HTTP client
  • FS ops
    • open
    • seek
    • mkdir
    • chmod
    • stat
    • read dir
    • etc. (the list if quite long)
  • Net
    • accept
    • connect
    • shutdown
    • listen
    • etc.
  • OS
    • exit
    • env
    • set/get env
    • hostname
    • load average
    • memory info
    • CPU info
  • Permissions
    • query
    • revoke
    • request
  • Open plugin
  • Process
    • run
    • kill
  • Runtime
    • compile
    • transpile
  • Signal
    • Bind
    • Unbind
    • Poll
  • Timer
    • Start
    • Stop
    • Now
    • Sleep sync
  • TLS
    • start
    • connect
    • listen
    • accept
  • Worker
    • Create a worker
    • Terminate worker
    • Get message
    • Post message
    • close
  • WebSocket
    • Create
    • Send
    • Close
    • Next event
The list of operations is rather extensive, and within it lies an intriguing observation. The majority of these operations are closely tied to the functions offered by the JavaScript component of the runtime, almost forming a direct one-to-one relationship. This can be illustrated with a couple of examples. Firstly, consider the "kill()" function found in the Process module; it aligns precisely with the "op_kill()" operation. Similarly, the "openSync()" function utilized for files corresponds harmoniously with the "op_open_sync()" operation. This parallel between the JavaScript functions and their corresponding operations underscores the intricate connection between Deno's runtime services and its JavaScript foundation.

Example

Let's take a look at the Deno.env.get() function in Deno as an illustrative example. This function serves as a straightforward and uncomplicated tool within the Deno programming environment. When we say it's a "sync" function, we mean that the JavaScript thread, which is the sequence of instructions being executed, will temporarily pause its execution until the desired outcome is obtained from a fundamental operation occurring at a lower level.

User program

Retrieving the value of an environment variable in Deno is a straightforward process. Let's explore how to obtain the value of the "HOME" variable using a simple code example:
Deno.env.get('HOME');

Deno code

The "env" object encompasses several useful APIs, including "get," "set," "delete," and "toObject." Upon examining its structure, it becomes evident that Deno.env.get corresponds to a function known as "getEnv."
Deno.env
{
get: [Function: getEnv],
toObject: [Function: toObject],
set: [Function: setEnv],
has: [Function: has],
delete: [Function: deleteEnv]
}
The getEnv functions is part of the OS service:
function getEnv(key) {
return ops.op_get_env(key) ?? undefined;
}
The getEnv() function is quite straightforward. Its main task is to initiate a basic action known as op_get_env(). This action operates at a lower level and is implemented in the Rust programming language. Exploring how this low-level Rust operation, often abbreviated as "OP," is triggered from Deno's JavaScript environment can be intriguing. However, delving into these technical intricacies might be best reserved for a later stage. For the time being, let's continue our journey and shift our focus towards examining the actual code behind op_get_env. This code originates from the Rust programming language, offering us insights into its inner workings and functionality. By doing so, we can gain a better understanding of the mechanics that power this aspect of Deno's functionality.
fn op_get_env(
state: &mut OpState,
key: String,
) -> Result<Option<String>, AnyError> {
let skip_permission_check = NODE_ENV_VAR_ALLOWLIST.contains(&key);
if !skip_permission_check {
state.borrow_mut::<PermissionsContainer>().check_env(&key)?;
}
if key.is_empty() {
return Err(type_error("Key is an empty string."));
}
if key.contains(&['=', '\0'] as &[char]) {
return Err(type_error(format!(
"Key contains invalid characters: {key:?}"
)));
}
let r = match env::var(key) {
Err(env::VarError::NotPresent) => None,
v => Some(v?),
};
Ok(r)
}
The Rust code involved here is rather straightforward. Its primary function is to carry out a series of validations in a single round. Subsequently, it proceeds to access the environment variable by utilizing Rust's convenient std::env crate. The outcome of this process is then conveyed as a value within the response. Notably, this response undertakes a journey, transitioning from the Rust code to the JS code. The intricacies of this connection between the two will be thoroughly explored in the forthcoming chapters of this book.
--
So far, we've covered the basics of ops. In the upcoming Chapter 6, we will delve deeper into ops, exploring their intricacies. We will gain a comprehensive understanding of the transition between JavaScript and Rust, both ways. This switch between the two programming languages plays a crucial role in the Deno environment, and in the next chapter, we will unravel its mechanics step by step.