Files
nvatom/lib/note-watcher.coffee

136 lines
4.6 KiB
CoffeeScript

chokidar = require 'chokidar'
fs = require 'fs'
lunr = require 'lunr'
path = require 'path'
_ = require 'underscore-plus'
zlib = require 'zlib'
{EventEmitter} = require 'events'
NoteCache = require './note-cache'
module.exports =
class NoteWatcher extends EventEmitter
@cacheVersion = 0
constructor: (@_baseDirectory, @_extensions, @_maxItems, @_enableLunrPipeline) ->
@_maxItems = @_maxItems ? 100
@_extensions = @_extensions ? ['.txt', '.md']
@_enableLunrPipeline = @_enableLunrPipeline ? false
@_noteCache = new NoteCache(@_baseDirectory, @_maxItems)
@_restoreSearchIndex() || @_initSearchIndex()
@_initWatcher()
save: ->
return unless @_state == 'ready'
cache = {
baseDirectory: @_baseDirectory,
extensions: @_extensions,
enableLunrPipeline: @_enableLunrPipeline,
version: @_cacheVersion,
searchIndex: JSON.stringify(@_searchIndex),
noteCache: @_noteCache.toJSON(),
}
fs.writeFileSync(
path.join(@_baseDirectory, 'nvatom.cache'),
zlib.deflateSync(JSON.stringify(cache)))
close: ->
@_watcher?.close()
search: (query) ->
if query? and query.length > 0
return @_searchIndex.search(query)
.slice(0, @_maxItems)
.map((x) => @_noteCache.getNote(x.ref))
else
return @_noteCache.getRecentNotes()
length: ->
@_noteCache.length()
_initSearchIndex: ->
@_state = 'initializing'
@_searchIndex = lunr(() ->
@field('title', { boost: 10 })
@field('body')
)
if !@_enableLunrPipeline
@_searchIndex.pipeline.reset()
_restoreSearchIndex: ->
cacheFilePath = path.join(@_baseDirectory, 'nvatom.cache')
return false unless fs.existsSync(cacheFilePath)
cache = JSON.parse(zlib.inflateSync(fs.readFileSync(cacheFilePath)))
return false if cache == null
return false unless _.isEqual(cache.baseDirectory, @_baseDirectory)
return false unless _.isEqual(cache.extensions, @_extensions)
return false unless _.isEqual(cache.enableLunrPipeline, @_enableLunrPipeline)
return false unless _.isEqual(cache.version, @_cacheVersion)
@_searchIndex = lunr.Index.load(JSON.parse(cache.searchIndex))
# TODO: Make this as an interface. Old note cache does not have to know upsert, remove, ready and so on.
# It only needs to know getNoteIds, hasNoteId, and getModifiedAt.
@_oldNoteCache = new NoteCache(@_baseDirectory, @_maxItems).load(JSON.parse(cache.noteCache))
@_state = 'recovering'
return true
_initWatcher: ->
options = {
ignored: (filePath, fileStat) =>
if fileStat?.isFile() then @_extensions.indexOf(path.extname(filePath)) < 0 else false
}
@_watcher = chokidar
.watch @_baseDirectory, options
.on 'add', (args) => @_add(args)
.on 'change', (args) => @_change(args)
.on 'unlink', (args) => @_unlink(args)
.on 'ready', () => @_ready()
_add: (filePath) ->
@_noteCache.upsert(@_toNoteId(filePath), fs.statSync(filePath).mtime)
if @_state != 'recovering'
@_searchIndex.add(@_toFileDetails(filePath))
@emit 'update', filePath
_change: (filePath) ->
@_noteCache.upsert(@_toNoteId(filePath), fs.statSync(filePath).mtime)
if @_state != 'recovering'
@_searchIndex.update(@_toFileDetails(filePath))
@emit 'update', filePath
_unlink: (filePath) ->
@_noteCache.remove(@_toNoteId(filePath))
if @_state != 'recovering'
@_searchIndex.remove(@_toFileDetails(filePath))
@emit 'update', filePath
_ready: () ->
@_noteCache.ready()
if @_state == 'recovering'
@_updateSearchIndex(@_oldNoteCache, @_noteCache)
delete @_oldNoteCache
@_state = 'ready'
@emit 'ready'
_updateSearchIndex: (oldNoteCache, newNoteCache) ->
for noteId in oldNoteCache.getNoteIds()
if !newNoteCache.hasNoteId(noteId)
@_searchIndex.remove(@_toFileDetails(@_toFilePath(noteId)))
else if oldNoteCache.getModifiedAt(noteId) <= newNoteCache.getModifiedAt(noteId)
@_searchIndex.update(@_toFileDetails(@_toFilePath(noteId)))
for noteId in newNoteCache.getNoteIds()
if !oldNoteCache.hasNoteId(noteId)
@_searchIndex.add(@_toFileDetails(@_toFilePath(noteId)))
_toNoteId: (filePath) ->
path.relative(@_baseDirectory, filePath)
_toFilePath: (noteId) ->
path.join(@_baseDirectory, noteId)
_toFileDetails: (filePath) ->
fileName = path.basename(filePath)
return {
id: @_toNoteId(filePath),
title: path.basename(fileName, path.extname(fileName)),
body: if fs.existsSync(filePath) then fs.readFileSync(filePath, { encoding: 'utf8' }) else ''
}