/// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright © 2023 Tony Garnock-Jones import { Bytes, Observe, Ref, Turn, stringify } from "@syndicate-lang/core"; import { QuasiValue as Q } from "@syndicate-lang/core"; import { service } from '@syndicate-lang/service'; import { Contents, File, asConfig, Config, Encoding, toEncoding } from './gen/fs.js'; import chokidar from 'chokidar'; import path from 'path'; import fs from 'fs'; export function main(_argv: string[]) { service(args => { const config = asConfig(args); if (config.awaitWriteFinish._variant === "invalid") { // (*A*) const s = stringify(config.awaitWriteFinish.awaitWriteFinish); throw new Error(`Invalid awaitWriteFinish configuration: ${s}`); } serve(config); }); } export function serve(config: Config) { at config.core.dataspace { during Observe({ "pattern": :pattern File({ "label": config.core.label, "path": \Q.lit($relativePath: string), "encoding": \Q.lit($encoding0), "contents": \_, }), }) => { const encoding = toEncoding(encoding0); if (encoding === void 0) return; if (path.isAbsolute(relativePath)) return; trackFile(config, relativePath, encoding); } } } function trackFile(config: Config, relativePath: string, encoding: Encoding) { const facet = Turn.activeFacet; const absolutePath = path.resolve(config.core.path, relativePath); field contents: Contents | undefined = void 0; at config.core.dataspace { assert File({ "label": config.core.label, "path": relativePath, "encoding": encoding, "contents": contents.value!, }) when (contents.value !== void 0); } let watcher: chokidar.FSWatcher | undefined = void 0; on stop { if (watcher !== void 0) { watcher.close(); watcher = void 0; } } const watchOptions: chokidar.WatchOptions = { cwd: config.core.path, disableGlobbing: true, depth: 0, }; switch (config.awaitWriteFinish._variant) { case "absent": case "invalid": // ruled out by check above marked (*A*) watchOptions.awaitWriteFinish = false; break; case "present": { const a = config.awaitWriteFinish.awaitWriteFinish; switch (a._variant) { case "simple": watchOptions.awaitWriteFinish = a.value; break; case "full": watchOptions.awaitWriteFinish = a; break; } break; } } watcher = chokidar.watch(absolutePath, watchOptions); function readContents() { const bytes = fs.readFileSync(absolutePath); switch (encoding._variant) { case 'utf8': try { const text = new TextDecoder('utf8', { fatal: true }).decode(bytes); contents.value = Contents.text(text); } catch { contents.value = Contents.invalidText(Bytes.from(bytes)); } break; case 'binary': contents.value = Contents.binary(Bytes.from(bytes)); break; } } function markAbsent() { contents.value = Contents.absent(); } watcher .on('ready', () => facet.turn(() => { if (contents.value === void 0) { markAbsent(); } })) .on('add', () => facet.turn(readContents)) .on('change', () => facet.turn(readContents)) .on('unlink', () => facet.turn(markAbsent)) .on('addDir', () => facet.turn(() => contents.value = Contents.directory())) .on('unlinkDir', () => facet.turn(markAbsent)); }