UNPKG

pauls-dat-api

Version:

Library of functions that make working with dat / hyperdrive easier.

228 lines (199 loc) 6.42 kB
const emitStream = require('emit-stream') const EventEmitter = require('events').EventEmitter const match = require('anymatch') const {sep} = require('path') const {findEntryByContentBlock, tonix} = require('./common') function watch (archive, path) { // options if (typeof path === 'string') { path = [path] } // handle by type if (!archive.key) { return watchFilesystem(archive, path) } else if (archive.writable) { return watchLocal(archive, path) } else { return watchRemote(archive, path) } } function watchFilesystem (fs, paths) { // create new emitter and stream var emitter = new EventEmitter() var stream = emitStream(emitter) // wire up events var stopwatch = fs.watch(sep, onFileChange) stream.on('close', () => { try { stopwatch() } catch (e) { /* ignore - this can happen if fs's path was invalid */ } }) function onFileChange (path) { // apply path matching path = tonix(path) path = temporaryWindowsPathFix(path, fs.base) if (paths && !match(paths, path)) { return } emitter.emit('changed', {path}) } return stream } function watchLocal (archive, paths) { // create new emitter and stream var emitter = new EventEmitter() var stream = emitStream(emitter) // wire up events archive.metadata.on('append', onMetaAppend) stream.on('close', () => { archive.metadata.removeListener('append', onMetaAppend) }) function onMetaAppend () { var block = archive.metadata.length - 1 archive.tree._getAndDecode(block, {}, (err, entry) => { if (err || !entry) return // apply path matching if (paths && !match(paths, entry.name)) { return } // local archive, emit both events immediately emitter.emit('invalidated', {path: entry.name}) emitter.emit('changed', {path: entry.name}) }) } return stream } function watchRemote (archive, paths) { // create new emitter and stream var emitter = new EventEmitter() var stream = emitStream(emitter) // wire up events archive.metadata.on('download', onMetaDownload) if (archive.content) { wireContent() } else { archive.on('content', wireContent) } function wireContent () { archive.content.on('download', onContentDownload) } stream.on('close', () => { // unlisten events archive.metadata.removeListener('download', onMetaDownload) if (archive.content) { archive.content.removeListener('download', onContentDownload) } }) // handlers function onMetaDownload (block) { archive.tree._getAndDecode(block, {}, (err, entry) => { if (err || !entry) return // apply path matching if (paths && !match(paths, entry.name)) { return } // emit emitter.emit('invalidated', {path: entry.name}) // check if we can emit 'changed' now var isChanged = false if (!entry.value) { isChanged = true // a deletion } else { var st = archive.tree._codec.decode(entry.value) var range = { start: st.offset, end: st.offset + st.blocks } isChanged = isDownloaded(archive, range) } if (isChanged) { emitter.emit('changed', {path: entry.name}) } }) } async function onContentDownload (block) { // find the entry this applies to var range = await findEntryByContentBlock(archive, block) // emit 'changed' if downloaded if (range && (!paths || match(paths, range.name)) && isDownloaded(archive, range)) { setImmediate(() => emitter.emit('changed', {path: range.name})) } } return stream } function isDownloaded (archive, range) { if (!archive.content || !archive.content.opened) return false for (var i = range.start; i < range.end; i++) { if (!archive.content.has(i)) return false } return true } function createNetworkActivityStream (archive, path) { // create new emitter and stream var emitter = new EventEmitter() var stream = emitStream(emitter) stream.on('close', () => { // unlisten events archive.metadata.removeListener('peer-add', onNetworkChanged) archive.metadata.removeListener('peer-remove', onNetworkChanged) untrack(archive.metadata, handlers.metadata) untrack(archive.content, handlers.content) }) // handlers function onNetworkChanged () { emitter.emit('network-changed', { connections: archive.metadata.peers.length }) } var handlers = { metadata: { onDownload (block, data) { emitter.emit('download', { feed: 'metadata', block, bytes: data.length }) }, onUpload (block, data) { emitter.emit('upload', { feed: 'metadata', block, bytes: data.length }) }, onSync () { emitter.emit('sync', { feed: 'metadata' }) } }, content: { onDownload (block, data) { emitter.emit('download', { feed: 'content', block, bytes: data.length }) }, onUpload (block, data) { emitter.emit('upload', { feed: 'content', block, bytes: data.length }) }, onSync () { emitter.emit('sync', { feed: 'content' }) } } } // initialize all trackers track(archive.metadata, 'metadata') if (archive.content) track(archive.content, 'content') else archive.on('content', () => track(archive.content, 'content')) archive.metadata.on('peer-add', onNetworkChanged) archive.metadata.on('peer-remove', onNetworkChanged) function track (feed, name) { if (!feed) return var h = handlers[name] feed.on('download', h.onDownload) feed.on('upload', h.onUpload) feed.on('sync', h.onSync) } function untrack (feed, handlers) { if (!feed) return feed.removeListener('download', handlers.onDownload) feed.removeListener('upload', handlers.onUpload) feed.removeListener('sync', handlers.onSync) } return stream } // HACK // workaround for a bug in libuv (https://github.com/nodejs/node/issues/19170) // paths will sometimes have some of the parent dir in them // if so, remove that bit // -prf function temporaryWindowsPathFix (path, parentPath) { if (process.platform === 'win32') { let secondSlashIndex = path.indexOf('/', 1) let firstSegment = path.slice(1, secondSlashIndex) if (parentPath.endsWith(firstSegment)) { return path.slice(secondSlashIndex) } } return path } module.exports = {watch, createNetworkActivityStream}