better code (watchLoad)

Massimo Melina committed Feb 11, 2022 at 11:31 UTC 37fb5132ddc4099aa2deb61b5d37622cf68eda2c
4 files changed +28 -21
src/config.ts
+2 -3
@@ -1,7 +1,6 @@
1 import EventEmitter from 'events'
2 import { argv } from './const'
3 import { watchLoad } from './watchLoad'
4 -import fs from 'fs/promises'
4 import yaml from 'yaml'
5 import _ from 'lodash'
6 import { debounceAsync, objSameKeys, onOffMap } from './misc'
@@ -19,7 +18,7 @@ let state: Record<string, any> = {}
18 const emitter = new EventEmitter()
19 emitter.setMaxListeners(10_000)
20 const path = argv.config || process.env.HFS_CONFIG || PATH
22 -watchLoad(path, values => setConfig(values, false), {
21 +const { save } = watchLoad(path, values => setConfig(values, false), {
22 failedOnFirstAttempt(){
23 console.log("No config file, using defaults")
24 setConfig({}, false)
@@ -114,7 +113,7 @@ export const saveConfigAsap = debounceAsync(async () => {
113 txt = ''
114 else
115 return
117 - fs.writeFile(path, txt)
116 + save(path, txt)
117 .catch(err => console.error('Failed at saving config file, please ensure it is writable.', String(err)))
118 })
119
src/perm.ts
+5 -12
@@ -1,4 +1,3 @@
1 -import fs from 'fs/promises'
1 import _ from 'lodash'
2 import yaml from 'yaml'
3 import { hashPassword } from './crypt'
@@ -81,25 +80,19 @@ export async function updateAccount(account: Account, changer?:Changer) {
80 }
81
82 let saving = false
84 -let justSaved = false
83 +let watchResult: ReturnType<typeof watchLoad>
84 const saveAccountsAsap = _.debounce(() => {
85 saving = true
87 - fs.writeFile(path, yaml.stringify({ accounts }, { lineWidth:1000 })) // we don't want big numbers to be folded
88 - .then(() => justSaved = true,
89 - err => console.error('Failed at saving accounts file, please ensure it is writable.', String(err)))
86 + watchResult.save(path, yaml.stringify({ accounts }, { lineWidth:1000 })) // we don't want big numbers to be folded
87 + .catch(err => console.error('Failed at saving accounts file, please ensure it is writable.', String(err)))
88 .finally(()=> saving = false)
89 }, 200) // group burst of requests
90
93 -let watcher: undefined | (()=>void)
91 subscribeConfig({ k:'accounts', defaultValue:'accounts.yaml' }, v => {
95 - watcher?.()
92 + watchResult?.unwatch()
93 if (!v)
94 return applyAccounts({})
98 - watcher = watchLoad(path = v, async data => {
99 - if (justSaved) {
100 - justSaved = false
101 - return
102 - }
95 + watchResult = watchLoad(path = v, async data => {
96 if (saving) return
97 const a = data?.accounts
98 if (!a)
src/plugins.ts
+1 -1
@@ -129,7 +129,7 @@ async function rescan() {
129 if (plugins[k]) // already loaded
130 continue
131 f = resolve(f) // without this, import won't work
132 - const unwatch = watchLoad(f, async () => {
132 + const { unwatch } = watchLoad(f, async () => {
133 try {
134 console.log(plugins[k] ? 'reloading plugin' : 'loading plugin', k)
135 const data = await import(f)
src/watchLoad.ts
+20 -5
@@ -1,4 +1,5 @@
1 import { FSWatcher, watch } from 'fs'
2 +import fs from 'fs/promises'
3 import yaml from 'yaml'
4 import { debounceAsync, readFileBusy } from './misc'
5
@@ -6,22 +7,36 @@ export type WatchLoadCanceller = () => void
7
8 interface Options { failedOnFirstAttempt?: ()=>void }
9
9 -export function watchLoad(path:string, parser:(data:any)=>void|Promise<void>, { failedOnFirstAttempt }:Options={}): WatchLoadCanceller {
10 +type WriteFile = typeof fs.writeFile
11 +interface WatchLoadReturn { unwatch:WatchLoadCanceller, save:WriteFile }
12 +export function watchLoad(path:string, parser:(data:any)=>void|Promise<void>, { failedOnFirstAttempt }:Options={}): WatchLoadReturn {
13 let doing = false
14 let watcher: FSWatcher | undefined
15 const debounced = debounceAsync(load, 500)
16 let retry: NodeJS.Timeout
17 + let saving: Promise<unknown> | undefined
18 init()
19 if (!watcher)
20 failedOnFirstAttempt?.()
17 - return () => {
18 - watcher?.close()
19 - clearTimeout(retry)
21 + return {
22 + unwatch(){
23 + watcher?.close()
24 + clearTimeout(retry)
25 + watcher = undefined
26 + },
27 + save(...args:Parameters<WriteFile>) {
28 + return Promise.resolve(saving).then(() => // wait in case another is ongoing
29 + saving = fs.writeFile(...args).finally(() => // save but also keep track of the current operation
30 + saving = undefined)) // clear
31 + }
32 }
33
34 function init() {
35 try {
24 - watcher = watch(path, debounced)
36 + watcher = watch(path, ()=> {
37 + if (!saving)
38 + debounced().then()
39 + })
40 debounced().then() // if file is not accessible watch will throw and we won't get here
41 }
42 catch {