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 tosuper()and with the “Symbolic Reference” asthisparameter - Define non-configurable wrappers for
delete()andcreate()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
SIGINTsent byCTRL+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()anddelete()can only be called once. In the case ofcreate(), 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
Referenceinstance 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 theProxytraps.create()can not use any asynchronous methods or be asynchronous itself, as that would make it return aPromisewhich 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 callingDeno.exit().In TypeScript, the
declarekeyword must be used when defining class properties. If you do not, the properties will be set to their default value orundefinedafter thesuper()andcreate()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;
static onExit(listener: () => void);
static offExit(listener: () => 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 tosuper() - Define non-configurable wrappers for
delete()andcreate()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()anddelete()can only be called once.An
Exitableinstance can create any circular reference it wants (SomethingRecyclableinstances 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 theRecyclablepart).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
declarekeyword 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
Exitableis to have it’s instances deleted when the program exits, all instances ofExitablewill never be garbage collected, so be careful when creating them.
Exitable also has two static methods: onExit() and offExit(). The first method allows you to specify a listener that gets called when the process is about to exit, and the second method allows you to remove a previously registered listener from the list of listeners to be called when the process is about to exit.
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.
Using onExit and offExit:
import { Exitable } from "https://deno.land/x/recyclable/mod.ts";
console.log('Hello, World!');
let listener1 = () => console.log('Goodbye, World!');
let listener2 = () => console.log('Goodbye, World! (again)');
Exitable.onExit(listener1);
Exitable.onExit(listener2);
Exitable.offExit(listener2);Output
Hello, World!
Goodbye, World!(If running Windows, this example requires the --allow-ffi and --unstable flags).
On this example we can see that when the process is about to exit listener1 gets called, but listener2 does not gets called because we remove it from the list before the process exits.
Known Issues
Certain signals cannot be handled as the OS does not allows them to be handled, that means that, despite
Recyclabletrying it’s best to make sure thatdelete()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
SIGHUPsimilarly 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.exeterminal is used, you’ll see thatSIGHUPis properly sent and intercepted.
Suggestions and Bugs 
Aaaand just like that you’ve reached the end! But remember, despite there being only 4 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