124 lines
4.0 KiB
TypeScript
124 lines
4.0 KiB
TypeScript
/// SPDX-License-Identifier: GPL-3.0-or-later
|
|
/// SPDX-FileCopyrightText: Copyright © 2023 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
|
|
|
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<Ref>) {
|
|
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<Ref>, 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));
|
|
}
|