Node.js has watch
and watchFile
in fs
module.
Chromium-based browsers (Chrome, Opera, Edge) have WICG File System Access implemented.
Let's create STDIO between the browser and Node.js using files.
Even though Node.js documentation recommends using watch()
, we'll use watchFile()
for this case. Just to see what happens.
Hear the spin when interval is set to 5 so we get input of lowercase letters and output of lowercase letters transformed to uppercase letters in order.
The Node.js part.
This assumes our watcher.js
script is in the local bin
directory on a Linux OS, thus ../Documents
to point to the files in Documents directory.
When the input
variable pointing to the file named stdin
we create in the browser is modified we read the file synchronously, write the data to the writable
side of a TransformStream
piped through a TextDecoderStream()
, then transform the data, in this case lower case letters, to uppercase letters and write
that transformed data to the output
variable, reflecting the file stdout
that we create(d) in the browser, then we write an empty string to the input
variable (stdin
) so that when we are done the process will resemble FIFO streams, nothing in the files.
// watcher.js
// node --experimental-default-type=module watcher.js
import { watch, watchFile, readFileSync, writeFileSync } from "node:fs";
const input = "../Documents/stdin";
const output = "../Documents/stdout";
const { readable, writable } = new TransformStream();
const writer = writable.getWriter();
readable.pipeThrough(new TextDecoderStream()).pipeTo(
new WritableStream({
write(value) {
writeFileSync(output, value.toUpperCase());
writeFileSync(input, "");
},
}),
);
watchFile(input, { interval: 5 }, () => {
writer.write(new Uint8Array(readFileSync(input)));
});
On the browser side we create two files, named stdin
and stdout
, respectively. FileSystemObserver
is not implemented,
so we'll essentially do the same thing we are doing in the Node.js script in the browser.
Technically we could use Blob().stream().pipeTo()
in the FileSystemObserver
callback to pipe the data to writable
side of our TransformStream
; however, during testing I encountered network errors. That's happended before during
other experiments in the past whether the error is file not found or similar, e.g., Capture 50 minutes of audio to file.
So we'll get an ArrayBuffer
representation of the data, pass that data to Response
and pipe Response.body
to avoid such Blob
based errors.
We pipe through a TextDecoderStream()
then pipe to a WritableStream()
and read our transformed data.
Notice the if (value === "z") {...}
. Because that's the last of our input from the browser to the Node.js script we don't overwrite that
last input with writeFileSync()
, leaving that Z
in output
, our stdout
file we created in the browser. So we'll go ahead and get rid
of that value when we're done streaming.
Of course that part can be improved, for the case where we don't really know what the last transformed data from the STDIO stream will be.
// fs.js
var [input, output] = await Promise.all(
["stdin", "stdout"].map((stdio) =>
showSaveFilePicker({
startIn: "documents",
suggestedName: stdio,
})
),
);
const { readable: stdout, writable: stdin } = new TransformStream();
stdout.pipeThrough(new TextDecoderStream()).pipeTo(
new WritableStream({
async write(value) {
console.log(value);
if (value === "Z") {
new Blob([]).stream().pipeTo(await output.createWritable());
}
},
}),
);
var fso = new FileSystemObserver(
async ([{ changedHandle, root, type }], record) => {
try {
if (type === "modified") {
new Response(await(await output.getFile()).arrayBuffer()).body.pipeTo(stdin, {
preventClose: true
});
}
} catch (e) {
console.warn(e);
}
},
);