import fs from 'fs'
import readline from 'readline' //已是nodejs內建函數, rollup為舊版無法偵測故會提示
import fsIsFile from './fsIsFile.mjs'
import evem from './evem.mjs'
import evEmit from './evEmit.mjs'
/**
* 使用stream從檔案讀utf8文字
*
* Unit Test: {@link https://github.com/yuda-lyu/wsemi/blob/master/test/fsBuildReadStreamText.test.mjs Github}
* @memberOf wsemi
* @param {String} fp 輸入讀取檔案路徑字串
* @returns {EventEmitter} 回傳EventEmitter,可監聽create、line、close、error事件,line事件接收讀入各列字串,create於回傳後延後派發故可被監聽。底層stream出錯(如開檔時檔案已消失、無讀取權限)以error事件回報{ fun: 'stream', msg }且隨後仍派發close。事件物件為原生EventEmitter(eventemitter3)。本模組於派發處以try攔截監聽器之同步拋錯,故其不會使行程崩潰,會改以error事件回報{ fun: 'listener', name, msg, args },無error監聽者則console.error;惟依EventEmitter規範,同一次派發中先拋錯之監聽器會中止該次派發,其後之監聽器不再被呼叫。async監聽器之reject不被攔截(規範上emit不觀察監聽器回傳值),須由監聽器自行處理,否則為unhandledRejection,監聽error時請先以fun欄位分流
* @example
* need test in nodejs.
*
* let test = async () => {
*
* let ms = []
*
* let fdt = './_test_fsBuildReadStreamText'
* fsCreateFolder(fdt) //創建臨時任務資料夾
*
* let fn = 't1.txt'
* let fp = `${fdt}/${fn}`
*
* fs.writeFileSync(fp, `abc
* 測試中文
* def123xyz
* `, 'utf8')
*
* let ev = fsBuildReadStreamText(fp)
*
* ev.on('create', () => {
* console.log('create')
* ms.push({ 'create': '' })
* })
*
* ev.on('line', (line) => {
* console.log('line', line)
* ms.push({ line })
* })
*
* let pm = genPm()
* ev.on('close', () => {
* console.log('close')
* pm.resolve()
* ms.push({ 'close': '' })
* })
* await pm
*
* fsDeleteFolder(fdt) //刪除臨時任務資料夾
*
* console.log('ms', ms)
* return ms
* }
* await test()
* // create
* // line abc
* // line 測試中文
* // line def123xyz
* // close
* // ms [
* // { create: '' },
* // { line: 'abc' },
* // { line: '測試中文' },
* // { line: 'def123xyz' },
* // { close: '' }
* // ]
*
*/
function fsBuildReadStreamText(fp) {
//check
if (!fsIsFile(fp)) {
throw new Error(`fp is not a file`)
}
//ev
let ev = evem() //line/close事件於readline與stream回呼內派發, 監聽器出錯不得殺行程, 故派發處以evEmit攔截
//stream
let stream = fs.createReadStream(fp, { encoding: 'utf8' })
//stream error, 底層stream之error(如open時檔案已消失ENOENT、無權限EACCES)無人監聽會殺行程, readline亦不轉發, 故轉為ev之error事件; stream出錯後會自行destroy並派發close
stream.on('error', (err) => {
evEmit(ev, 'error', [{ fun: 'stream', msg: err }], { tag: 'fsBuildReadStreamText' })
})
//create, 須延後至呼叫端取得ev並註冊監聽器後才派發(同步emit時呼叫端尚未取得ev, 永遠監聽不到); nextTick早於任何I/O回呼, 而stream之open與read皆為非同步I/O, 故create必先於error、line與close
process.nextTick(() => {
evEmit(ev, 'create', [], { tag: 'fsBuildReadStreamText' })
})
//rl
let rl = readline.createInterface({
input: stream,
crlfDelay: Infinity, //支援\r\n或\n
})
//rl line
rl.on('line', (line) => {
// console.log(`line`,line)
evEmit(ev, 'line', [line], { tag: 'fsBuildReadStreamText' })
})
//rl error, 新版nodejs(實測v24)之readline會將input之error再於Interface上emit('error'), 無監聽者同樣殺行程; 同一錯誤已由stream.on('error')轉發, 此處僅吸收避免重複派發
rl.on('error', () => {})
// //rl close
// rl.on('close', () => {
// // console.log('close')
// })
//stream close
stream.on('close', () => {
//stream close事件才代表檔案可刪除
evEmit(ev, 'close', [], { tag: 'fsBuildReadStreamText' })
})
// //stream end
// stream.on('end', () => {
// //stream end事件只是讀寫結束, 不代表檔案可刪除
// })
return ev
}
export default fsBuildReadStreamText