main
ts 83 lines 3.11 KB
Raw
1 // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3 import { FSWatcher, watch } from 'fs'
4 import fs from 'fs/promises'
5 import { readFileWithBusyRetry } from './util-files'
6 import { debounceAsync } from './debounceAsync'
7 import { BetterEventEmitter } from './events'
8
9 type WatchLoadCanceller = () => void
10
11 interface Options { failedOnFirstAttempt?: ()=>void, immediateFirst?: boolean }
12
13 type WriteFile = (data: string, options?: { reparse: boolean }) => Promise<void>
14 interface WatchLoadReturn { unwatch:WatchLoadCanceller, save: WriteFile, emitter: BetterEventEmitter, getText: () => string | undefined, getPath: () => string }
15 export function watchLoad(path:string, parser:(data:any)=>void|Promise<void>, { failedOnFirstAttempt, immediateFirst }:Options={}): WatchLoadReturn {
16 let doing = false
17 let watcher: FSWatcher | undefined
18 const debounced = debounceAsync(load, { wait: 500, maxWait: 1000, reuseRunning: true })
19 let retry: NodeJS.Timeout
20 let last: string | undefined
21 const emitter = new BetterEventEmitter()
22 const save = debounceAsync(async (data: string, { reparse=false }={}) => {
23 await fs.writeFile(path, data, 'utf8')
24 last = data
25 if (reparse)
26 await parser(data)
27 emitter.emit('change', last)
28 })
29 install(true)
30 return { unwatch, save, emitter, getText: () => last, getPath: () => path }
31
32 function install(first=false) {
33 try {
34 watcher = watch(path, () => {
35 if (!save.isWorking())
36 void debounced()
37 })
38 debounced().catch(x=>x)
39 if (immediateFirst && first)
40 void debounced.flush()
41 }
42 catch(e) {
43 retry = setTimeout(install, 3_000) // manual watching until watch is successful
44 if (first)
45 failedOnFirstAttempt?.()
46 }
47 }
48
49 function unwatch() {
50 watcher?.close()
51 clearTimeout(retry)
52 watcher = undefined
53 }
54
55 async function load(){
56 if (doing) return
57 doing = true
58 try {
59 await save.flush() // apply pending saves first
60 const text = await readFileWithBusyRetry(path).catch(e => { // ignore read errors
61 if (e.code === 'EPERM')
62 console.error("Missing permissions on file", path) // warn user, who could be clueless about this problem
63 // keep the last good content on transient read failures so we don't apply accidental "empty file" state
64 if (e.code === 'ENOENT')
65 return ''
66 throw e
67 })
68 if (text === last)
69 return
70 last = text
71 emitter.emit('change', last)
72 console.debug('Loaded', path)
73 unwatch(); install() // reinstall, as the original file could have been renamed. We watch by the name.
74 await parser(text)
75 }
76 catch(e) {
77 console.error("Error loading", path, String(e))
78 }
79 finally {
80 doing = false
81 }
82 }
83 }