convex
Version:
Client for the Convex Cloud
460 lines (459 loc) • 12.8 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
import { chalkStderr } from "chalk";
import stdFs from "fs";
import * as fsPromises from "fs/promises";
import os from "os";
import path from "path";
import crypto from "crypto";
const tmpDirOverrideVar = "CONVEX_TMPDIR";
function tmpDirPath() {
const envTmpDir = process.env[tmpDirOverrideVar];
return envTmpDir ?? os.tmpdir();
}
const tmpDirRoot = tmpDirPath();
let warned = false;
function warnCrossFilesystem(dstPath) {
const dstDir = path.dirname(dstPath);
if (!warned) {
console.warn(
chalkStderr.yellow(
`Temporary directory '${tmpDirRoot}' and project directory '${dstDir}' are on different filesystems.`
)
);
console.warn(
chalkStderr.gray(
` If you're running into errors with other tools watching the project directory, override the temporary directory location with the ${chalkStderr.bold(
tmpDirOverrideVar
)} environment variable.`
)
);
console.warn(
chalkStderr.gray(
` Be sure to pick a temporary directory that's on the same filesystem as your project.`
)
);
warned = true;
}
}
export async function withTmpDir(callback) {
const tmpPath = stdFs.mkdtempSync(path.join(tmpDirRoot, "convex"));
const tmpDir = {
writeUtf8File(contents) {
const filePath = path.join(tmpPath, crypto.randomUUID());
nodeFs.writeUtf8File(filePath, contents);
return filePath;
},
registerTempPath(st) {
const filePath = path.join(tmpPath, crypto.randomUUID());
nodeFs.registerPath(filePath, st);
return filePath;
},
writeFileStream(path2, stream, onData) {
return nodeFs.writeFileStream(path2, stream, onData);
},
path: tmpPath
};
try {
await callback(tmpDir);
} finally {
stdFs.rmSync(tmpPath, { force: true, recursive: true });
}
}
export class NodeFs {
listDir(dirPath) {
return stdFs.readdirSync(dirPath, { withFileTypes: true });
}
exists(path2) {
try {
stdFs.statSync(path2);
return true;
} catch (e) {
if (e.code === "ENOENT") {
return false;
}
throw e;
}
}
stat(path2) {
return stdFs.statSync(path2);
}
readUtf8File(path2) {
return stdFs.readFileSync(path2, { encoding: "utf-8" });
}
createReadStream(path2, options) {
return stdFs.createReadStream(path2, options);
}
// To avoid issues with filesystem events triggering for our own streamed file
// writes, writeFileStream is intentionally not on the Filesystem interface
// and not implemented by RecordingFs.
async writeFileStream(path2, stream, onData) {
const fileHandle = await fsPromises.open(path2, "wx", 420);
try {
for await (const chunk of stream) {
if (onData) {
onData(chunk);
}
await fileHandle.write(chunk);
}
} finally {
await fileHandle.close();
}
}
access(path2) {
return stdFs.accessSync(path2);
}
writeUtf8File(path2, contents, mode) {
const fd = stdFs.openSync(path2, "w", mode);
try {
stdFs.writeFileSync(fd, contents, { encoding: "utf-8" });
stdFs.fsyncSync(fd);
} finally {
stdFs.closeSync(fd);
}
}
mkdir(dirPath, options) {
try {
stdFs.mkdirSync(dirPath, { recursive: options?.recursive });
} catch (e) {
if (options?.allowExisting && e.code === "EEXIST") {
return;
}
throw e;
}
}
rmdir(path2) {
stdFs.rmdirSync(path2);
}
unlink(path2) {
return stdFs.unlinkSync(path2);
}
swapTmpFile(fromPath, toPath) {
try {
return stdFs.renameSync(fromPath, toPath);
} catch (e) {
if (e.code === "EXDEV") {
warnCrossFilesystem(toPath);
stdFs.copyFileSync(fromPath, toPath);
return;
}
throw e;
}
}
registerPath(_path, _st) {
}
invalidate() {
}
}
export const nodeFs = new NodeFs();
export class RecordingFs {
constructor(traceEvents) {
// Absolute path -> Set of observed child names
__publicField(this, "observedDirectories", /* @__PURE__ */ new Map());
// Absolute path -> observed stat (or null if observed nonexistent)
__publicField(this, "observedFiles", /* @__PURE__ */ new Map());
// Have we noticed that files have changed while recording?
__publicField(this, "invalidated", false);
__publicField(this, "traceEvents");
this.traceEvents = traceEvents;
}
listDir(dirPath) {
const absDirPath = path.resolve(dirPath);
const dirSt = nodeFs.stat(absDirPath);
this.registerNormalized(absDirPath, dirSt);
const entries = nodeFs.listDir(dirPath);
for (const entry of entries) {
const childPath = path.join(absDirPath, entry.name);
const childSt = nodeFs.stat(childPath);
this.registerPath(childPath, childSt);
}
const observedNames = new Set(entries.map((e) => e.name));
const existingNames = this.observedDirectories.get(absDirPath);
if (existingNames) {
if (!setsEqual(observedNames, existingNames)) {
if (this.traceEvents) {
console.log(
"Invalidating due to directory children mismatch",
observedNames,
existingNames
);
}
this.invalidated = true;
}
}
this.observedDirectories.set(absDirPath, observedNames);
return entries;
}
exists(path2) {
try {
const st = nodeFs.stat(path2);
this.registerPath(path2, st);
return true;
} catch (err) {
if (err.code === "ENOENT") {
this.registerPath(path2, null);
return false;
}
throw err;
}
}
stat(path2) {
try {
const st = nodeFs.stat(path2);
this.registerPath(path2, st);
return st;
} catch (err) {
if (err.code === "ENOENT") {
this.registerPath(path2, null);
}
throw err;
}
}
readUtf8File(path2) {
try {
const st = nodeFs.stat(path2);
this.registerPath(path2, st);
return nodeFs.readUtf8File(path2);
} catch (err) {
if (err.code === "ENOENT") {
this.registerPath(path2, null);
}
throw err;
}
}
createReadStream(path2, options) {
try {
const st = nodeFs.stat(path2);
this.registerPath(path2, st);
return nodeFs.createReadStream(path2, options);
} catch (err) {
if (err.code === "ENOENT") {
this.registerPath(path2, null);
}
throw err;
}
}
access(path2) {
try {
const st = nodeFs.stat(path2);
this.registerPath(path2, st);
return nodeFs.access(path2);
} catch (err) {
if (err.code === "ENOENT") {
this.registerPath(path2, null);
}
throw err;
}
}
writeUtf8File(filePath, contents, mode) {
const absPath = path.resolve(filePath);
nodeFs.writeUtf8File(filePath, contents, mode);
this.updateOnWrite(absPath);
}
mkdir(dirPath, options) {
const absPath = path.resolve(dirPath);
try {
stdFs.mkdirSync(absPath, { recursive: options?.recursive });
} catch (e) {
if (options?.allowExisting && e.code === "EEXIST") {
const st = nodeFs.stat(absPath);
this.registerNormalized(absPath, st);
return;
}
throw e;
}
this.updateOnWrite(absPath);
}
rmdir(dirPath) {
const absPath = path.resolve(dirPath);
stdFs.rmdirSync(absPath);
this.updateOnDelete(absPath);
}
unlink(filePath) {
const absPath = path.resolve(filePath);
stdFs.unlinkSync(absPath);
this.updateOnDelete(absPath);
}
swapTmpFile(fromPath, toPath) {
const absToPath = path.resolve(toPath);
nodeFs.swapTmpFile(fromPath, absToPath);
this.updateOnWrite(absToPath);
}
updateOnWrite(absPath) {
const newSt = nodeFs.stat(absPath);
this.observedFiles.set(absPath, newSt);
const parentPath = path.resolve(path.dirname(absPath));
const observedParent = this.observedDirectories.get(parentPath);
if (observedParent !== void 0) {
observedParent.add(path.basename(absPath));
}
}
updateOnDelete(absPath) {
this.observedFiles.set(absPath, null);
const parentPath = path.resolve(path.dirname(absPath));
const observedParent = this.observedDirectories.get(parentPath);
if (observedParent !== void 0) {
observedParent.delete(path.basename(absPath));
}
}
registerPath(p, st) {
const absPath = path.resolve(p);
this.registerNormalized(absPath, st);
}
invalidate() {
this.invalidated = true;
}
registerNormalized(absPath, observed) {
const existing = this.observedFiles.get(absPath);
if (existing !== void 0) {
const stMatch = stMatches(observed, existing);
if (!stMatch.matches) {
if (this.traceEvents) {
console.log(
"Invalidating due to st mismatch",
absPath,
observed,
existing,
stMatch.reason
);
}
this.invalidated = true;
}
}
this.observedFiles.set(absPath, observed);
}
finalize() {
if (this.invalidated) {
return "invalidated";
}
return new Observations(this.observedDirectories, this.observedFiles);
}
}
export class Observations {
constructor(directories, files) {
__publicField(this, "directories");
__publicField(this, "files");
this.directories = directories;
this.files = files;
}
paths() {
const out = [];
for (const path2 of this.directories.keys()) {
out.push(path2);
}
for (const path2 of this.files.keys()) {
out.push(path2);
}
return out;
}
overlaps({
absPath
}) {
let currentSt;
try {
currentSt = nodeFs.stat(absPath);
} catch (e) {
if (e.code === "ENOENT") {
currentSt = null;
} else {
throw e;
}
}
const observedSt = this.files.get(absPath);
if (observedSt !== void 0) {
const stMatch = stMatches(observedSt, currentSt);
if (!stMatch.matches) {
const reason = `modified (${stMatch.reason})`;
return { overlaps: true, reason };
}
}
const parentPath = path.resolve(path.dirname(absPath));
const observedParent = this.directories.get(parentPath);
if (observedParent !== void 0) {
const filename = path.basename(absPath);
if (currentSt === null && observedParent.has(filename)) {
return { overlaps: true, reason: "deleted" };
}
if (currentSt !== null && !observedParent.has(filename)) {
return { overlaps: true, reason: "added" };
}
}
return { overlaps: false };
}
}
function setsEqual(a, b) {
if (a.size !== b.size) {
return false;
}
for (const elem of a.keys()) {
if (!b.has(elem)) {
return false;
}
}
return true;
}
export function stMatches(a, b) {
if (a === null && b === null) {
return { matches: true };
}
if (a !== null && b !== null) {
if (a.dev !== b.dev) {
return { matches: false, reason: "device boundary" };
}
if (a.isFile() || b.isFile()) {
if (!a.isFile() || !b.isFile()) {
return { matches: false, reason: "file type" };
}
if (a.ino !== b.ino) {
return {
matches: false,
reason: `file inode (${a.ino} vs. ${b.ino})`
};
}
if (a.size !== b.size) {
return {
matches: false,
reason: `file size (${a.size} vs. ${b.size})`
};
}
if (a.mtimeMs !== b.mtimeMs) {
return {
matches: false,
reason: `file mtime (${a.mtimeMs} vs. ${b.mtimeMs})`
};
}
return { matches: true };
}
if (a.isDirectory() || b.isDirectory()) {
if (!b.isDirectory() || !b.isDirectory()) {
return { matches: false, reason: "dir file type" };
}
if (a.ino !== b.ino) {
return {
matches: false,
reason: `dir inode (${a.ino} vs. ${b.ino})`
};
}
return { matches: true };
}
if (a.ino !== b.ino) {
return {
matches: false,
reason: `special inode (${a.ino} vs. ${b.ino})`
};
}
return { matches: true };
}
return { matches: false, reason: "deleted mismatch" };
}
export function consistentPathSort(a, b) {
for (let i = 0; i < Math.min(a.name.length, b.name.length); i++) {
if (a.name.charCodeAt(i) !== b.name.charCodeAt(i)) {
return a.name.charCodeAt(i) - b.name.charCodeAt(i);
}
}
return a.name.length - b.name.length;
}
//# sourceMappingURL=fs.js.map