This article is a part of the systemic-ts series.
complication
complication

Source of Complication: No Destructor

Some languages have destructors, but not JavaScript/TypeScript. Some argue that it is unnecessary for scripts.

If you must write a system that juggles around file and socket handles, chances are, you will forget to close some of them. So I argue that it has been necessary even since NodeJS.

// https://nodejs.org/api/fs.html#fscreatewritestreampath-options
const writeStream = fs.createWriteStream(somePath, {
  autoClose: false,
});

If you open enough of these kind of non-auto-closing streams and forget them, it will cause you "too many open file errors", for example. In a simple script, it is dismissable as a beginner's mistake. In a long-running system, resource leaks can cause contentions (i.e. different parts of the same system competing for a resource) and unnecessary context switches.

By not having a destructor, we cannot tell the runtime to close a file handle, socket handle, and any other side-effect-ish constructs whenever there is no more reference pointing to it.

Relation to layered lifetime

Related: class vs layered lifetime

Destructor not existing correlates with the lifetime management. The common fix to this problem is separating resource owners (e.g. who makes and destroys streams) from resource operators (who pump bytes into the streams).

const withStream = async (somePath: string, fns: ((stream: WritableStream) => Promise<unknown>)[]) => {
  // initialize resource
  const writeStream = fs.createWriteStream(somePath, {
    autoClose: false;
  });
  // do whatever you want with it
  await Promise.all(fns.map(() => fn(stream)))
  writeStream.close()
}

Separating users from ownership follows a similar pattern as GC.

// A total oversimplification!
const SystemWithGC = async () => {
  // initialize resource management system
  const resourceContext = createResourceContext();
  const instructionAndSoOn = createInstructionsQueue();
  while (true) {
    // operate on resource
    await instructionAndSoOn.run(resourceContext, {
      parkAfterInMilliseconds: 500,
    });
    // manage resource
    await resourceContext.markAndSweep();
  }
};

"Can I Copy Your Homework? Yeah just change it up a bit so it doesn't look obvious you copied."

If you squint your eyes, the pattern looks similar to data-oriented design, although it has a different purpose and details.

// Another total oversimplification!
const fn = async () => {
  const uniformData1 = [...]
  const uniformData2 = [...]
  const uniformData3 = [...]
  
  await instruction1(uniformData1)
  await instruction2(uniformData2)
  await instruction3(uniformData3)
}

Footnote:

At the time of writing, there are a proposal and a solution. The latter is hard to use because of how the rest of the language has been designed. You can tell by the many red warnings that approximately say "Don't touch unless you're absolutely sure."

complication
complication