Im trying to understand what's going on here with the behavior of this code from the book Node.js Design Patterns, 3rd edition
// using esm modules
// index.mjs
import { readFile } from 'fs';
const cache = new Map()
function inconsistentRead(filename, cb) {
console.log('INSIDE INCONSISTENT READ, filename is: ', filename)
console.log('Cache: ', cache)
if (cache.has(filename)) {
console.log('filename in cache, getting from cache: ', cache)
// invoked synchronously
cb(cache.get(filename))
} else {
// async function
console.log('running readFile from fs')
readFile(filename, 'utf8', (err, data) => {
console.log('inside callback passed into readFile, setting cache')
cache.set(filename, data);
console.log('about to call callback with data = ', data)
cb(data);
})
}
}
function createFileReader(filename) {
const listeners = []
console.log('listeners (empty at first): ', listeners)
inconsistentRead(filename, (value) => {
console.log('inconsistent read callback invoked, listeners: ', listeners)
listeners.forEach((listener) => listener(value));
})
return {
onDataReady: (listener) => {
console.log("about to push listener to listeners", listeners)
listeners.push(listener)
console.log('after pushing to listeners: ', listeners)
}
}
}
const reader1 = createFileReader('data.txt')
console.log('before reader1.ondataready')
reader1.onDataReady((data) => {
console.log(`First call data: ${data}`);
})
The following is the output of the above:
listeners (empty at first): []
INSIDE INCONSISTENT READ, filename is: data.txt
Cache: Map(0) {}
running readFile from fs
before reader1.ondataready
about to push listener to listeners []
after pushing to listeners: [ [Function (anonymous)] ]
inside callback passed into readFile, setting cache
about to call callback with data = some basic data
inconsistent read callback invoked, listeners: [ [Function (anonymous)] ]
First call data: some basic data
What im confused on is the behavior of readFile.
Read file doesn't call the callback until we populate the callbacks array listeners
. I was expecting when it was ready to call console.log('inconsistent read callback invoked, listeners: ', listeners)
but instead it doesn't call it until after we push into the listeners array.
Why is the callback in readFile getting called after populating listeners? Is it possible due to timing for it to call the callback where listeners.forEach(...)
happens before it gets populated?
const c = closure2()
This line calls closure2 and returns object with onDataReady. Once you call onDataReady by:
c.onDataReady('test1')
It calls the onDataReady function which pushes the string in the array. If you observe the documentation, you will see that array.push returns the count of array elements after update and hence you get output like 1,2,3...
It won't call
arr.forEach(a => { console.log('ARR ELEMENT: ', a) })
anytime after the first time closure2() is called and hence prints nothing since the array is empty initially.
If you want to print the complete array after every push/addition of data, you can do something like this:
function closure2() {
const arr = [];
return {
print: () => arr.forEach(a => {
console.log('ARR ELEMENT: ', a)
}),
onDataReady: (arrElement) => arr.push(arrElement)
}
}
const c = closure2()
c.onDataReady('test1')
c.print(); // PRINTS ["test1"]
c.onDataReady('test2')
c.print() // PRINTS ["test1", "test2"]
c.onDataReady('test3')
c.print() // PRINTS ["test1", "test2", "test3"]
c.onDataReady('test4')
c.print() // PRINTS ["test1", "test2", "test3", "test4"]
EDITED ANSWER BASED ON OP's EDIT
Yes, the callback will get called after populating the listeners in the code you shared. This is how Javascript works. There is no timing issue here. Even if you try to read an empty file, the callback will get executed later. Javascript will first execute all the synchronous code till the call stack is empty after which it looks at the callback functions present in the task queues(microtask queue and macrotask queue). Since the reading of the file, which is an I/O operation is a macro task, it will execute its callback after the call stack is empty or after every other sequential synchronous code has already been executed. Below are some good resources to help understand this behavior.