Skip to main content
Deno 2 is finally here 🎉️
Learn more


A TypeScript library for Deno designed to provide a high-level API to manage low-level resources.

Recyclable allows you to free and/or close your resources automatically, preventing memory leaks and unnecessary memory usage. It uses V8’s Garbage Collection system to know when to clean up, and, if the object is never collected, then it will be free’d right before the program exits.


Advantages


  • Cross-Platform. Works on Windows, Linux and MacOS (Not tested on MacOS yet, but should work).
  • Portable. Zero dependencies
  • Types. Includes types declarations

Table of Contents



Changelog


You can read the changelogs on Recyclable’s Releases page


Quick Start


To use Recyclable, simply import it into your project like this:

import { Recyclable, Exitable } from "https://deno.land/x/recyclable/mod.ts";

Then, extend the Recyclable or Exitable class and implement the create() and delete() methods.

class SomeClass extends Recyclable /** Or Exitable **/ {
    create() {
        // Initialize and allocate the object here
    }
  
    delete() {
        // Clean up and free the resources here
    }
}

IMPORTANT: Recyclable must be the first module you import in your proyect, as it relies on Monkey-patching the Deno.exit(), Deno.addSignalListener() and Deno.removeSignalListener() methods. Also, to handle SIGHUP on Windows, it needs to use SetConsoleCtrlHandler which may cause some stability issues if it’s not called right away. Until Deno adds proper way of handling exit events, doing this is the only option.

Regarding the flags, on Linux/MacOS you don’t need any. On Windows, you need --unstable and --allow-ffi. This is to handle the SIGHUP event which ins’t accesible through Deno’s APIs.


Documentation


Recyclable exports two classes, which are very similar. The first one has the following implementation:

abstract class Recyclable<Parameters extends unknown[] = []> {
    constructor(...params: Parameters);
    
    abstract create(...params: Parameters): void;
    abstract delete(): void;
}

You must extend the Recyclable class to use it, and then, when an instance of your class is constructed, the underlying Recyclable ‘s constructor will…

  • Create a “Symbolic Reference” to the actual instance, which will be returned by the class
  • Call create() with the same arguments that were passed to super() and with the “Symbolic Reference” as this parameter
  • Define non-configurable wrappers for delete() and create() in the instance to prevent calling them more than once

Since a “Symbolic Reference” (Made via Proxying the real instance) is returned, it’s possible to detect when the object is out of reach while mainting the original instance available so it can be used as this parameter for the delete() function, which will be called when…

  • The “Symbolic Reference” instance is garbage collected
  • Deno.exit() gets called
  • A process-terminating signal (Like SIGINT sent by CTRL+C) is received
  • No code to execute is left

And in the case that a delete() method throws while being called right before exiting, it will print the uncaught error to stderr but won’t prevent the rest of delete() methods in queue from being called, ensuring that every object is free’d before exiting.

However, this also introduces some limitations:

  • create() and delete() can only be called once. In the case of create(), it is called when constructing the instance, so in practice, it can’t never be called. In the other hand, delete() is called automatically, unless you have called it before manually (However the purpose of this library is that you don’t have to… so why would you? ).

  • A Reference instance can not create any circular references of the instance during any moment at runtime (circulars of other objects inside of it are allowed, just not of the instance itself), otherwise, an error will be thrown by the Proxy traps.

  • create() can not use any asynchronous methods or be asynchronous itself, as that would make it return a Promise which is unsupported.

  • delete() can not use any asynchronous methods or be asynchronous itself, as that would require the program to continue executing in circumstances where it shouldn’t like when calling Deno.exit().

  • In TypeScript, the declare keyword must be used when defining class properties. If you do not, the properties will be set to their default value or undefined after the super() and create() calls return.

  • In JavaScript, class properties that are initialized by create() cannot be specified in the class body nor have default values for the same reason listed above.


The second exported class is Exitable, which has the following implementation:

abstract class Exitable<Parameters extends unknown[] = []> {
    constructor(...params: Parameters);
    
    abstract create(...params: Parameters): void;
    abstract delete(): void;
}

Just like Recyclable, You must extend the Exitable class to use it, and then, when an instance of your class is constructed, the underlying Exitable ‘s constructor will…

  • Call create() with the same arguments that were passed to super()
  • Define non-configurable wrappers for delete() and create() in the instance to prevent calling them more than once

Exitable is pretty much the same as Recyclable, with the main difference that is not Garbage Collected and is only deleted when the app somehow exits. As same with Recyclable, Exitable also follows some rules and limitations:

  • Just like Recyclable, create() and delete() can only be called once.

  • An Exitable instance can create any circular reference it wants (Something Recyclable instances can not do). This is due to not needing garbage collection to ocurr, therefore, no “Symbolic References” are needed (You can read about them above on the Recyclable part).

  • create() can not use any asynchronous methods or be asynchronous itself.

  • delete() can not use any asynchronous methods or be asynchronous itself.

  • In TypeScript, the declare keyword must be used when defining class properties and on JavaScript no default values must be used on the class body. Recyclable’s rules explain the reason why.

  • Since the idea behind Exitable is to have it’s instances deleted when the program exits, all instances of Exitable will never be garbage collected, so be careful when creating them.


Usage


Let’s take a look at some examples to understand better how to use Recyclable. Our first example shows us how to use Deno.dlopen and Recyclable together.

Using Deno.dlopen with Recyclable:

import { Recyclable } from "https://deno.land/x/recyclable/mod.ts";

class User32 extends Recyclable {
    // Don't forget to use the declare keyword
    declare library: Deno.DynamicLibrary<{
        MessageBoxA: {
            parameters: [ 'i32', 'buffer', 'buffer', 'i32' ],
            result: 'i32'
        }
    }>;

    create() {
        console.log('Opening the user32.dll library');
        
        this.library = Deno.dlopen('user32', {
            MessageBoxA: {
                parameters: [ 'i32', 'buffer', 'buffer', 'i32' ],
                result: 'i32'
            }
        })
    }

    delete() {
        console.log('Closing the user32.dll library');
        this.library.close();
    }
}

let encoder = new TextEncoder();
let user32  = new User32();

function CString(str: string) {
    return encoder.encode(`${str}\0`).buffer;
}

user32.library.symbols.MessageBoxA(0, CString('Hello world!'), CString('My Message Box'), 0);
// No need to manually call user32.library.close() Yey! 🥳

Output:

Opening the user32.dll library
Closing the user32.dll library

(This example is only for Windows and requires the --allow-ffi and --unstable flags).

In this example User32::delete() was called and closed the Deno.DynamicLibrary even thought we never explicitly called that function. That is thanks to the Recyclable class calling the method right before exiting.



Creating a Program with Exitable:

import { Exitable } from "https://deno.land/x/recyclable/mod.ts";

class Program extends Exitable<[name: string]> {
    // Don't forget to use the declare keyword
    declare name: string;
    declare interval: number;

    create(name: string) {
        console.log(`Starting ${name}`);
        
        this.name = name;
        this.interval = setInterval(() => {
            console.log(`An event from ${name}`);
        }, 500)
    }

    delete() {
        console.log(`Goodbye from ${this.name}`);
        clearInterval(this.interval);
    }
}

new Program('MyApp');

Output

Starting MyApp
An event from MyApp
An event from MyApp
-- (CTRL-C is pressed) --
Goodbye from MyApp
^C

(If running Windows, this example requires the --allow-ffi and --unstable flags).

This example shows that Program::create() is called when the Program instance is created and that Program::delete() is called when the process exits in any way.



Using Deno.FsFile with Recyclable:

import { Recyclable } from "https://deno.land/x/recyclable/mod.ts";

class WritableFile extends Recyclable<[filepath: string]> {
    // Don't forget to use the declare keyword
    declare filepath: string;
    declare encoder: TextEncoder;
    declare fsFile: Deno.FsFile;

    create(filepath: string) {
        console.log(`Opening a handle to ${filepath}`);
        
        this.filepath = filepath;
        this.encoder = new TextEncoder();
        this.fsFile = Deno.openSync(filepath, { write: true, create: true });
    }

    delete() {
        console.log(`Closing the handle to ${this.filepath}`);
        this.fsFile.close();
    }

    write(contents: string) {
        let buffer = this.encoder.encode(contents);
        this.fsFile.writeSync(buffer);

        return buffer.length;
    }
}

let myFile = new WritableFile('./greeting.txt');
let written = myFile.write('Hello, World!');

console.log(`${written} bytes written to ${myFile.filepath}. Time to exit`);
Deno.exit(0);
// No need to manually call myFile.fsFile.close() Yey! 🥳

Output

Opening a handle to ./greeting.txt
13 bytes written to ./greeting.txt. Time to exit
Closing the handle to ./greeting.txt

(This example requires the --allow-write flag, and, if running Windows, the --allow-ffi and --unstable flags).

This example is very interesting, as we can see that Deno.exit() is called without WritableFile::delete() being called before, yet it is called anyways right after Deno.exit() gets called without preventing the program from exiting.


Known Issues


  • Certain signals cannot be handled as the OS does not allows them to be handled, that means that, despite Recyclable trying it’s best to make sure that delete() is called before exiting, it is not guaranteed under the circumstance that an unhandleable signal is sent. However, if the user is sending these signals, it should know that not a single program will be able to gracefully exit, so…

  • Recyclable can’t run cleanup tasks if your app is frozen (Like when it’s stuck on an infinite loop). So make sure to not get the Event Loop blocked otherwise no JavaScript code will be able to run at all.

  • Recyclable handles SIGHUP similarly to Node.js, meaning it faces the same limitation: the brand new Windows Terminal’s closing event cannot be intercepted. This is due to the way this terminal kills processes . However, if good old cmd.exe terminal is used, you’ll see that SIGHUP is properly sent and intercepted.


Suggestions and Bugs


Aaaand just like that you’ve reached the end! But remember, despite there being only 2 examples, there are many more uses you can give to this library! If you have any suggestions, or want to report a bug, please do so by creating an issue at Recyclable’s Repository.


License


Recyclable is licensed under the MIT License. By using it, you accept the conditions and limitations specified in the LICENSE file.

Copyright © 2023 @jabonkun