File watcher support for vfs
This commit is contained in:
parent
aa0cdddbbd
commit
de8e8b3381
|
@ -210,6 +210,200 @@ namespace collections {
|
|||
}
|
||||
}
|
||||
|
||||
export class SortedSet<T> {
|
||||
private _comparer: (a: T, b: T) => number;
|
||||
private _values: T[] = [];
|
||||
private _order: number[] | undefined;
|
||||
private _version = 0;
|
||||
private _copyOnWrite = false;
|
||||
|
||||
constructor(comparer: ((a: T, b: T) => number) | SortOptions<T>, iterable?: Iterable<T>) {
|
||||
this._comparer = typeof comparer === "object" ? comparer.comparer : comparer;
|
||||
this._order = typeof comparer === "object" && comparer.sort === "insertion" ? [] : undefined;
|
||||
if (iterable) {
|
||||
const iterator = getIterator(iterable);
|
||||
try {
|
||||
for (let i = nextResult(iterator); i; i = nextResult(iterator)) {
|
||||
const value = i.value;
|
||||
this.add(value);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
closeIterator(iterator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get size() {
|
||||
return this._values.length;
|
||||
}
|
||||
|
||||
public get comparer() {
|
||||
return this._comparer;
|
||||
}
|
||||
|
||||
public get [Symbol.toStringTag]() {
|
||||
return "SortedSet";
|
||||
}
|
||||
|
||||
public has(key: T) {
|
||||
return ts.binarySearch(this._values, key, ts.identity, this._comparer) >= 0;
|
||||
}
|
||||
|
||||
public add(value: T) {
|
||||
const index = ts.binarySearch(this._values, value, ts.identity, this._comparer);
|
||||
if (index >= 0) {
|
||||
this._values[index] = value;
|
||||
}
|
||||
else {
|
||||
this.writePreamble();
|
||||
insertAt(this._values, ~index, value);
|
||||
if (this._order) insertAt(this._order, ~index, this._version);
|
||||
this.writePostScript();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public delete(value: T) {
|
||||
const index = ts.binarySearch(this._values, value, ts.identity, this._comparer);
|
||||
if (index >= 0) {
|
||||
this.writePreamble();
|
||||
ts.orderedRemoveItemAt(this._values, index);
|
||||
if (this._order) ts.orderedRemoveItemAt(this._order, index);
|
||||
this.writePostScript();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public clear() {
|
||||
if (this.size > 0) {
|
||||
this.writePreamble();
|
||||
this._values.length = 0;
|
||||
if (this._order) this._order.length = 0;
|
||||
this.writePostScript();
|
||||
}
|
||||
}
|
||||
|
||||
public forEach(callback: (value: T, key: T, collection: this) => void, thisArg?: any) {
|
||||
const values = this._values;
|
||||
const indices = this.getIterationOrder();
|
||||
const version = this._version;
|
||||
this._copyOnWrite = true;
|
||||
try {
|
||||
if (indices) {
|
||||
for (const i of indices) {
|
||||
callback.call(thisArg, values[i], values[i], this);
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (const value of values) {
|
||||
callback.call(thisArg, value, value, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (version === this._version) {
|
||||
this._copyOnWrite = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public * keys() {
|
||||
const values = this._values;
|
||||
const indices = this.getIterationOrder();
|
||||
const version = this._version;
|
||||
this._copyOnWrite = true;
|
||||
try {
|
||||
if (indices) {
|
||||
for (const i of indices) {
|
||||
yield values[i];
|
||||
}
|
||||
}
|
||||
else {
|
||||
yield* values;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (version === this._version) {
|
||||
this._copyOnWrite = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public * values() {
|
||||
const values = this._values;
|
||||
const indices = this.getIterationOrder();
|
||||
const version = this._version;
|
||||
this._copyOnWrite = true;
|
||||
try {
|
||||
if (indices) {
|
||||
for (const i of indices) {
|
||||
yield values[i];
|
||||
}
|
||||
}
|
||||
else {
|
||||
yield* values;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (version === this._version) {
|
||||
this._copyOnWrite = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public * entries() {
|
||||
const values = this._values;
|
||||
const indices = this.getIterationOrder();
|
||||
const version = this._version;
|
||||
this._copyOnWrite = true;
|
||||
try {
|
||||
if (indices) {
|
||||
for (const i of indices) {
|
||||
yield [values[i], values[i]] as [T, T];
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (const value of values) {
|
||||
yield [value, value] as [T, T];
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (version === this._version) {
|
||||
this._copyOnWrite = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public [Symbol.iterator]() {
|
||||
return this.values();
|
||||
}
|
||||
|
||||
private writePreamble() {
|
||||
if (this._copyOnWrite) {
|
||||
this._values = this._values.slice();
|
||||
if (this._order) this._order = this._order.slice();
|
||||
this._copyOnWrite = false;
|
||||
}
|
||||
}
|
||||
|
||||
private writePostScript() {
|
||||
this._version++;
|
||||
}
|
||||
|
||||
private getIterationOrder() {
|
||||
if (this._order) {
|
||||
const order = this._order;
|
||||
return this._order
|
||||
.map((_, i) => i)
|
||||
.sort((x, y) => order[x] - order[y]);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function insertAt<T>(array: T[], index: number, value: T): void {
|
||||
if (index === 0) {
|
||||
array.unshift(value);
|
||||
|
|
481
src/harness/unittests/vfs.ts
Normal file
481
src/harness/unittests/vfs.ts
Normal file
|
@ -0,0 +1,481 @@
|
|||
// tslint:disable:object-literal-key-quotes
|
||||
describe("vfs", () => {
|
||||
function describeFileSystem(title: string, ignoreCase: boolean, root: string) {
|
||||
describe(title, () => {
|
||||
describe("statSync", () => {
|
||||
it("ok (file)", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { time: 0, files: { [root]: { "file": "testing" } } });
|
||||
fs.time(1);
|
||||
const stats = fs.statSync("file");
|
||||
assert.isTrue(stats.isFile());
|
||||
assert.strictEqual(stats.nlink, 1);
|
||||
assert.strictEqual(stats.size, 7);
|
||||
assert.strictEqual(stats.atimeMs, 0);
|
||||
assert.strictEqual(stats.mtimeMs, 0);
|
||||
assert.strictEqual(stats.ctimeMs, 0);
|
||||
assert.strictEqual(stats.birthtimeMs, 0);
|
||||
});
|
||||
|
||||
it("ok (file, hardlink)", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { time: 0, files: { [root]: { "file": "testing" } } });
|
||||
fs.time(1);
|
||||
fs.linkSync("file", "link");
|
||||
const stats = fs.statSync("link");
|
||||
assert.isTrue(stats.isFile());
|
||||
assert.strictEqual(stats.nlink, 2);
|
||||
assert.strictEqual(stats.size, 7);
|
||||
assert.strictEqual(stats.atimeMs, 0);
|
||||
assert.strictEqual(stats.mtimeMs, 0);
|
||||
assert.strictEqual(stats.ctimeMs, 1);
|
||||
assert.strictEqual(stats.birthtimeMs, 0);
|
||||
});
|
||||
|
||||
it("ok (file, symlink)", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { time: 0, files: { [root]: { "file": "testing" } } });
|
||||
fs.time(1);
|
||||
fs.symlinkSync("file", "symlink");
|
||||
const stats = fs.statSync("symlink");
|
||||
assert.isTrue(stats.isFile());
|
||||
assert.strictEqual(stats.nlink, 1);
|
||||
assert.strictEqual(stats.size, 7);
|
||||
assert.strictEqual(stats.atimeMs, 0);
|
||||
assert.strictEqual(stats.mtimeMs, 0);
|
||||
assert.strictEqual(stats.ctimeMs, 0);
|
||||
assert.strictEqual(stats.birthtimeMs, 0);
|
||||
});
|
||||
|
||||
it("ok (directory)", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { time: 0, files: { [root]: { "dir": {} } } });
|
||||
fs.time(1);
|
||||
const stats = fs.statSync("dir");
|
||||
assert.isTrue(stats.isDirectory());
|
||||
assert.strictEqual(stats.nlink, 1);
|
||||
assert.strictEqual(stats.size, 0);
|
||||
assert.strictEqual(stats.atimeMs, 0);
|
||||
assert.strictEqual(stats.mtimeMs, 0);
|
||||
assert.strictEqual(stats.ctimeMs, 0);
|
||||
assert.strictEqual(stats.birthtimeMs, 0);
|
||||
});
|
||||
|
||||
it("ok (directory, symlink)", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { time: 0, files: { [root]: { "dir": {} } } });
|
||||
fs.time(1);
|
||||
fs.symlinkSync("dir", "symlink");
|
||||
const stats = fs.statSync("symlink");
|
||||
assert.isTrue(stats.isDirectory());
|
||||
assert.strictEqual(stats.nlink, 1);
|
||||
assert.strictEqual(stats.size, 0);
|
||||
assert.strictEqual(stats.atimeMs, 0);
|
||||
assert.strictEqual(stats.mtimeMs, 0);
|
||||
assert.strictEqual(stats.ctimeMs, 0);
|
||||
assert.strictEqual(stats.birthtimeMs, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("lstatSync", () => {
|
||||
it("ok (file)", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { time: 0, files: { [root]: { "file": "testing" } } });
|
||||
fs.time(1);
|
||||
const stats = fs.lstatSync("file");
|
||||
assert.isTrue(stats.isFile());
|
||||
assert.strictEqual(stats.nlink, 1);
|
||||
assert.strictEqual(stats.size, 7);
|
||||
assert.strictEqual(stats.atimeMs, 0);
|
||||
assert.strictEqual(stats.mtimeMs, 0);
|
||||
assert.strictEqual(stats.ctimeMs, 0);
|
||||
assert.strictEqual(stats.birthtimeMs, 0);
|
||||
});
|
||||
|
||||
it("ok (file, hardlink)", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { time: 0, files: { [root]: { "file": "testing" } } });
|
||||
fs.time(1);
|
||||
fs.linkSync("file", "link");
|
||||
const stats = fs.lstatSync("link");
|
||||
assert.isTrue(stats.isFile());
|
||||
assert.strictEqual(stats.nlink, 2);
|
||||
assert.strictEqual(stats.size, 7);
|
||||
assert.strictEqual(stats.atimeMs, 0);
|
||||
assert.strictEqual(stats.mtimeMs, 0);
|
||||
assert.strictEqual(stats.ctimeMs, 1);
|
||||
assert.strictEqual(stats.birthtimeMs, 0);
|
||||
});
|
||||
|
||||
it("ok (file, symlink)", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { time: 0, files: { [root]: { "file": "testing" } } });
|
||||
fs.time(1);
|
||||
fs.symlinkSync("file", "symlink");
|
||||
const stats = fs.lstatSync("symlink");
|
||||
assert.isTrue(stats.isSymbolicLink());
|
||||
assert.strictEqual(stats.nlink, 1);
|
||||
assert.strictEqual(stats.size, 4);
|
||||
assert.strictEqual(stats.atimeMs, 1);
|
||||
assert.strictEqual(stats.mtimeMs, 1);
|
||||
assert.strictEqual(stats.ctimeMs, 1);
|
||||
assert.strictEqual(stats.birthtimeMs, 1);
|
||||
});
|
||||
|
||||
it("ok (directory)", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { time: 0, files: { [root]: { "dir": {} } } });
|
||||
fs.time(1);
|
||||
const stats = fs.lstatSync("dir");
|
||||
assert.isTrue(stats.isDirectory());
|
||||
assert.strictEqual(stats.nlink, 1);
|
||||
assert.strictEqual(stats.size, 0);
|
||||
assert.strictEqual(stats.atimeMs, 0);
|
||||
assert.strictEqual(stats.mtimeMs, 0);
|
||||
assert.strictEqual(stats.ctimeMs, 0);
|
||||
assert.strictEqual(stats.birthtimeMs, 0);
|
||||
});
|
||||
|
||||
it("ok (directory, symlink)", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { time: 0, files: { [root]: { "dir": {} } } });
|
||||
fs.time(1);
|
||||
fs.symlinkSync("dir", "symlink");
|
||||
const stats = fs.lstatSync("symlink");
|
||||
assert.isTrue(stats.isSymbolicLink());
|
||||
assert.strictEqual(stats.nlink, 1);
|
||||
assert.strictEqual(stats.size, 3);
|
||||
assert.strictEqual(stats.atimeMs, 1);
|
||||
assert.strictEqual(stats.mtimeMs, 1);
|
||||
assert.strictEqual(stats.ctimeMs, 1);
|
||||
assert.strictEqual(stats.birthtimeMs, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("readdirSync", () => {
|
||||
it("ok", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "a": {} } } });
|
||||
const actual = fs.readdirSync(root);
|
||||
assert.deepEqual(actual, ["a"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mkdirSync", () => {
|
||||
it("ok", () => {
|
||||
const path = vpath.combine(root, "a");
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: {} } });
|
||||
fs.mkdirSync(path);
|
||||
assert.isTrue(fs.statSync(path).isDirectory());
|
||||
});
|
||||
});
|
||||
|
||||
describe("rmdirSync", () => {
|
||||
it("ok", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "a": {} } } });
|
||||
fs.rmdirSync(vpath.combine(root, "a"));
|
||||
const actual = fs.readdirSync(root);
|
||||
assert.deepEqual(actual.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("linkSync", () => {
|
||||
it("ok", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "file": "" } } });
|
||||
fs.linkSync("file", "link");
|
||||
const stats1 = fs.statSync("file");
|
||||
const stats2 = fs.statSync("link");
|
||||
assert.deepEqual(stats2, stats1);
|
||||
assert.strictEqual(stats1.nlink, 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unlinkSync", () => {
|
||||
it("ok (nlink = 1)", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "file": "" } } });
|
||||
fs.unlinkSync("file");
|
||||
assert.throws(() => fs.statSync("file"), /ENOENT/);
|
||||
});
|
||||
|
||||
it("ok (nlink > 1)", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "file": "" } } });
|
||||
fs.linkSync("file", "link");
|
||||
fs.unlinkSync("file");
|
||||
assert.throws(() => fs.statSync("file"), /ENOENT/);
|
||||
const stats = fs.statSync("link");
|
||||
assert.strictEqual(stats.nlink, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("renameSync", () => {
|
||||
it("ok", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "file": "" } } });
|
||||
const stats1 = fs.statSync("file");
|
||||
fs.renameSync("file", "renamed");
|
||||
assert.throws(() => fs.statSync("file"), /ENOENT/);
|
||||
const stats2 = fs.statSync("renamed");
|
||||
assert.strictEqual(stats2.ino, stats1.ino);
|
||||
});
|
||||
});
|
||||
|
||||
describe("symlinkSync", () => {
|
||||
it("ok (absolute target)", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "file": "test" } } });
|
||||
fs.symlinkSync(vpath.combine(root, "file"), "symlink");
|
||||
assert.strictEqual(fs.readFileSync("symlink", "utf8"), "test");
|
||||
});
|
||||
|
||||
it("ok (relative target)", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "file": "test" } } });
|
||||
fs.symlinkSync("file", "symlink");
|
||||
assert.strictEqual(fs.readFileSync("symlink", "utf8"), "test");
|
||||
});
|
||||
|
||||
it("ok (indirect target)", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "file": "test" } } });
|
||||
fs.symlinkSync("file", "symlink1");
|
||||
fs.symlinkSync("symlink1", "symlink2");
|
||||
assert.strictEqual(fs.readFileSync("symlink2", "utf8"), "test");
|
||||
});
|
||||
});
|
||||
|
||||
describe("realpathSync", () => {
|
||||
it("ok (absolute target)", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "file": "test" } } });
|
||||
fs.symlinkSync(vpath.combine(root, "file"), "symlink");
|
||||
assert.strictEqual(fs.realpathSync("symlink"), vpath.combine(root, "file"));
|
||||
});
|
||||
|
||||
it("ok (relative target)", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "file": "test" } } });
|
||||
fs.symlinkSync("file", "symlink");
|
||||
assert.strictEqual(fs.realpathSync("symlink"), vpath.combine(root, "file"));
|
||||
});
|
||||
|
||||
it("ok (indirect target)", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "file": "test" } } });
|
||||
fs.symlinkSync("file", "symlink1");
|
||||
fs.symlinkSync("symlink1", "symlink2");
|
||||
assert.strictEqual(fs.realpathSync("symlink2"), vpath.combine(root, "file"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("readFileSync", () => {
|
||||
it("ok", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "file": "test" } } });
|
||||
assert.strictEqual(fs.readFileSync("file", "utf8"), "test");
|
||||
});
|
||||
});
|
||||
|
||||
describe("writeFileSync", () => {
|
||||
it("ok", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "file": "test" } } });
|
||||
fs.writeFileSync("file", "replacement", "utf8");
|
||||
assert.strictEqual(fs.readFileSync("file", "utf8"), "replacement");
|
||||
});
|
||||
});
|
||||
|
||||
describe("mount", () => {
|
||||
it("ok", () => {
|
||||
const other = new vfs.FileSystem(/*ignoreCase*/ false, { files: {
|
||||
"/": {
|
||||
"subdir": {},
|
||||
"file": ""
|
||||
}
|
||||
} });
|
||||
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: {} } });
|
||||
fs.mountSync("/", vpath.combine(root, "dir"), other);
|
||||
|
||||
const names = fs.readdirSync(vpath.combine(root, "dir"));
|
||||
assert.deepEqual(names, ["file", "subdir"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shadow", () => {
|
||||
it("ok", () => {
|
||||
const rootFs = new vfs.FileSystem(ignoreCase, { files: { [root]: { } } }).makeReadonly();
|
||||
const shadowFs = rootFs.shadow();
|
||||
assert.strictEqual(shadowFs.shadowRoot, rootFs);
|
||||
});
|
||||
|
||||
it("shadow reads from root", () => {
|
||||
const rootFs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "dir": {}, "file": "test" } } }).makeReadonly();
|
||||
const shadowFs = rootFs.shadow();
|
||||
assert.deepEqual(shadowFs.readdirSync(root), ["dir", "file"]);
|
||||
assert.strictEqual(shadowFs.readFileSync("file", "utf8"), "test");
|
||||
});
|
||||
|
||||
it("shadow write does not affect root", () => {
|
||||
const rootFs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "dir": {}, "file": "test" } } }).makeReadonly();
|
||||
const shadowFs = rootFs.shadow();
|
||||
shadowFs.writeFileSync("file", "replacement", "utf8");
|
||||
assert.strictEqual(rootFs.readFileSync("file", "utf8"), "test");
|
||||
});
|
||||
});
|
||||
|
||||
describe("meta", () => {
|
||||
it("ok", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "file": "" } } });
|
||||
fs.filemeta("file").set("testKey", "testValue");
|
||||
assert.strictEqual(fs.filemeta("file").get("testKey"), "testValue");
|
||||
});
|
||||
|
||||
it("shadow inherits from root", () => {
|
||||
const rootFs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "file": "" } } });
|
||||
rootFs.filemeta("file").set("testKey", "testValue");
|
||||
rootFs.makeReadonly();
|
||||
const shadowFs = rootFs.shadow();
|
||||
assert.strictEqual(shadowFs.filemeta("file").get("testKey"), "testValue");
|
||||
});
|
||||
|
||||
it("shadow inherits from root with mutation", () => {
|
||||
const rootFs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "file": "" } } });
|
||||
rootFs.makeReadonly();
|
||||
const shadowFs = rootFs.shadow();
|
||||
rootFs.filemeta("file").set("testKey", "testValue");
|
||||
assert.strictEqual(shadowFs.filemeta("file").get("testKey"), "testValue");
|
||||
});
|
||||
|
||||
it("shadow does not mutate root", () => {
|
||||
const rootFs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "file": "" } } });
|
||||
rootFs.filemeta("file").set("testKey", "testValue");
|
||||
rootFs.makeReadonly();
|
||||
const shadowFs = rootFs.shadow();
|
||||
shadowFs.filemeta("file").set("testKey", "newValue");
|
||||
assert.strictEqual(rootFs.filemeta("file").get("testKey"), "testValue");
|
||||
});
|
||||
});
|
||||
|
||||
describe("watch", () => {
|
||||
it("writeFile() new file in directory", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "dir": {} } } });
|
||||
const invocations: [string, string][] = [];
|
||||
const callback = (eventType: string, filename: string) => { invocations.push([eventType, filename]); };
|
||||
|
||||
fs.watch(vpath.combine(root, "dir"), callback);
|
||||
fs.writeFileSync("dir/file", "test");
|
||||
|
||||
assert.deepEqual(invocations, [
|
||||
["rename", "file"],
|
||||
["change", "file"]
|
||||
]);
|
||||
});
|
||||
it("writeFile() replace file in directory", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "dir": { "file": "" } } } });
|
||||
const invocations: [string, string][] = [];
|
||||
const callback = (eventType: string, filename: string) => { invocations.push([eventType, filename]); };
|
||||
|
||||
fs.watch(vpath.combine(root, "dir"), callback);
|
||||
fs.writeFileSync("dir/file", "test");
|
||||
|
||||
assert.deepEqual(invocations, [
|
||||
["change", "file"]
|
||||
]);
|
||||
});
|
||||
it("rename() file in directory", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "dir": { "file": "test" } } } });
|
||||
const invocations: [string, string][] = [];
|
||||
const callback = (eventType: string, filename: string) => { invocations.push([eventType, filename]); };
|
||||
|
||||
fs.watch(vpath.combine(root, "dir"), callback);
|
||||
fs.renameSync(vpath.combine(root, "dir/file"), vpath.combine(root, "dir/file1"));
|
||||
|
||||
assert.deepEqual(invocations, [
|
||||
["rename", "file"],
|
||||
["rename", "file1"]
|
||||
]);
|
||||
});
|
||||
it("link() new file in directory", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "dir": { "file": "test" } } } });
|
||||
const invocations: [string, string][] = [];
|
||||
const callback = (eventType: string, filename: string) => { invocations.push([eventType, filename]); };
|
||||
|
||||
fs.watch(vpath.combine(root, "dir"), callback);
|
||||
fs.linkSync(vpath.combine(root, "dir/file"), vpath.combine(root, "dir/file1"));
|
||||
|
||||
assert.deepEqual(invocations, [
|
||||
["rename", "file1"]
|
||||
]);
|
||||
});
|
||||
it("symlink() file in directory", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "dir": { "file": "test" } } } });
|
||||
const invocations: [string, string][] = [];
|
||||
const callback = (eventType: string, filename: string) => { invocations.push([eventType, filename]); };
|
||||
|
||||
fs.watch(vpath.combine(root, "dir"), callback);
|
||||
fs.symlinkSync(vpath.combine(root, "dir/file"), vpath.combine(root, "dir/file1"));
|
||||
|
||||
assert.deepEqual(invocations, [
|
||||
["rename", "file1"]
|
||||
]);
|
||||
});
|
||||
it("unlink() file in directory (single link)", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "dir": { "file": "" } } } });
|
||||
const invocations: [string, string][] = [];
|
||||
const callback = (eventType: string, filename: string) => { invocations.push([eventType, filename]); };
|
||||
|
||||
fs.watch(vpath.combine(root, "dir"), callback);
|
||||
fs.unlinkSync(vpath.combine(root, "dir/file"));
|
||||
|
||||
assert.deepEqual(invocations, [
|
||||
["rename", "file"]
|
||||
]);
|
||||
});
|
||||
it("unlink() file in directory (multiple links)", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "dir": { "file": "", "file1": new vfs.Link("file") } } } });
|
||||
const invocations: [string, string][] = [];
|
||||
const callback = (eventType: string, filename: string) => { invocations.push([eventType, filename]); };
|
||||
|
||||
fs.watch(vpath.combine(root, "dir"), callback);
|
||||
fs.unlinkSync(vpath.combine(root, "dir/file"));
|
||||
|
||||
assert.deepEqual(invocations, [
|
||||
["rename", "file"]
|
||||
]);
|
||||
});
|
||||
it("mkdir() new subdirectory in directory", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "dir": { } } } });
|
||||
const invocations: [string, string][] = [];
|
||||
const callback = (eventType: string, filename: string) => { invocations.push([eventType, filename]); };
|
||||
|
||||
fs.watch(vpath.combine(root, "dir"), callback);
|
||||
fs.mkdirSync(vpath.combine(root, "dir/subdir"));
|
||||
|
||||
assert.deepEqual(invocations, [
|
||||
["rename", "subdir"]
|
||||
]);
|
||||
});
|
||||
it("rmdir() subdirectory in directory", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "dir": { "subdir": {} } } } });
|
||||
const invocations: [string, string][] = [];
|
||||
const callback = (eventType: string, filename: string) => { invocations.push([eventType, filename]); };
|
||||
|
||||
fs.watch(vpath.combine(root, "dir"), callback);
|
||||
fs.rmdirSync(vpath.combine(root, "dir/subdir"));
|
||||
|
||||
assert.deepEqual(invocations, [
|
||||
["rename", "subdir"]
|
||||
]);
|
||||
});
|
||||
it("writeFile() replace file", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "dir": { "file": "" } } } });
|
||||
const invocations: [string, string][] = [];
|
||||
const callback = (eventType: string, filename: string) => { invocations.push([eventType, filename]); };
|
||||
|
||||
fs.watch(vpath.combine(root, "dir/file"), callback);
|
||||
fs.writeFileSync("dir/file", "test");
|
||||
|
||||
assert.deepEqual(invocations, [
|
||||
["change", "file"]
|
||||
]);
|
||||
});
|
||||
it("unlink() file (single link)", () => {
|
||||
const fs = new vfs.FileSystem(ignoreCase, { files: { [root]: { "dir": { "file": "" } } } });
|
||||
const invocations: [string, string][] = [];
|
||||
const callback = (eventType: string, filename: string) => { invocations.push([eventType, filename]); };
|
||||
|
||||
fs.watch(vpath.combine(root, "dir/file"), callback);
|
||||
fs.unlinkSync(vpath.combine(root, "dir/file"));
|
||||
|
||||
assert.deepEqual(invocations, [
|
||||
["rename", "file"]
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describeFileSystem("posix", /*ignoreCase*/ false, /*root*/ "/");
|
||||
describeFileSystem("win32", /*ignoreCase*/ true, /*root*/ "c:/");
|
||||
});
|
||||
// tslint:enable:object-literal-key-quotes
|
|
@ -43,18 +43,25 @@ namespace vfs {
|
|||
links?: collections.SortedMap<string, Inode>;
|
||||
shadows?: Map<number, Inode>;
|
||||
meta?: collections.Metadata;
|
||||
watchedFiles?: collections.SortedMap<string, Set<WatchedFile>>;
|
||||
watchers?: collections.SortedMap<string, FSWatcherEntrySet>;
|
||||
} = {};
|
||||
|
||||
private _cwd: string; // current working directory
|
||||
private _time: number | Date | (() => number | Date);
|
||||
private _shadowRoot: FileSystem | undefined;
|
||||
private _dirStack: string[] | undefined;
|
||||
private _timers: FileSystemTimers;
|
||||
private _noRecursiveWatchers: boolean;
|
||||
private _directoryStructureVersion = 0;
|
||||
|
||||
constructor(ignoreCase: boolean, options: FileSystemOptions = {}) {
|
||||
const { time = -1, files, meta } = options;
|
||||
const { time = -1, files, meta, timers = { setInterval, clearInterval }, noRecursiveWatchers = false } = options;
|
||||
this.ignoreCase = ignoreCase;
|
||||
this.stringComparer = this.ignoreCase ? vpath.compareCaseInsensitive : vpath.compareCaseSensitive;
|
||||
this._time = time;
|
||||
this._timers = timers;
|
||||
this._noRecursiveWatchers = noRecursiveWatchers;
|
||||
|
||||
if (meta) {
|
||||
for (const key of Object.keys(meta)) {
|
||||
|
@ -641,6 +648,7 @@ namespace vfs {
|
|||
node.size = node.buffer.byteLength;
|
||||
node.mtimeMs = time;
|
||||
node.ctimeMs = time;
|
||||
this._notifySelf(node, "change");
|
||||
}
|
||||
|
||||
private _mknod(dev: number, type: typeof S_IFREG, mode: number, time?: number): FileInode;
|
||||
|
@ -655,7 +663,8 @@ namespace vfs {
|
|||
mtimeMs: time,
|
||||
ctimeMs: time,
|
||||
birthtimeMs: time,
|
||||
nlink: 0
|
||||
nlink: 0,
|
||||
incomingLinks: new Map<DirectoryInode, collections.SortedSet<string>>()
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -663,15 +672,40 @@ namespace vfs {
|
|||
links.set(name, node);
|
||||
node.nlink++;
|
||||
node.ctimeMs = time;
|
||||
if (parent) parent.mtimeMs = time;
|
||||
if (!parent && !this._cwd) this._cwd = name;
|
||||
this._invalidatePaths();
|
||||
|
||||
let set = node.incomingLinks.get(parent);
|
||||
if (!set) node.incomingLinks.set(parent, set = new collections.SortedSet(this.stringComparer));
|
||||
set.add(name);
|
||||
|
||||
if (parent) {
|
||||
parent.mtimeMs = time;
|
||||
this._notifyChild(parent, "rename", name);
|
||||
this._notifyAncestors(parent, "change");
|
||||
}
|
||||
else if (!this._cwd) {
|
||||
this._cwd = name;
|
||||
}
|
||||
}
|
||||
|
||||
private _removeLink(parent: DirectoryInode | undefined, links: collections.SortedMap<string, Inode>, name: string, node: Inode, time = this.time()) {
|
||||
links.delete(name);
|
||||
node.nlink--;
|
||||
node.ctimeMs = time;
|
||||
if (parent) parent.mtimeMs = time;
|
||||
this._invalidatePaths();
|
||||
|
||||
const set = node.incomingLinks.get(parent);
|
||||
if (set) {
|
||||
set.delete(name);
|
||||
if (set.size === 0) node.incomingLinks.delete(parent);
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
parent.mtimeMs = time;
|
||||
this._notifyChild(parent, "rename", name);
|
||||
this._notifyAncestors(parent, "change");
|
||||
this._removeWatchers(parent, name);
|
||||
}
|
||||
}
|
||||
|
||||
private _replaceLink(oldParent: DirectoryInode, oldLinks: collections.SortedMap<string, Inode>, oldName: string, newParent: DirectoryInode, newLinks: collections.SortedMap<string, Inode>, newName: string, node: Inode, time: number) {
|
||||
|
@ -684,6 +718,17 @@ namespace vfs {
|
|||
oldLinks.set(newName, node);
|
||||
oldParent.mtimeMs = time;
|
||||
newParent.mtimeMs = time;
|
||||
this._invalidatePaths();
|
||||
|
||||
const set = node.incomingLinks.get(oldParent);
|
||||
if (set) {
|
||||
set.delete(oldName);
|
||||
set.add(newName);
|
||||
}
|
||||
|
||||
this._notifyChild(oldParent, "rename", oldName);
|
||||
this._notifyChild(newParent, "rename", newName);
|
||||
this._notifyAncestors(newParent, "change");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -799,6 +844,28 @@ namespace vfs {
|
|||
return node.buffer;
|
||||
}
|
||||
|
||||
private _invalidatePaths() {
|
||||
this._directoryStructureVersion++;
|
||||
}
|
||||
|
||||
private _getPaths(node: Inode): ReadonlyArray<string> {
|
||||
if (!node.paths || node.pathsVersion !== this._directoryStructureVersion) {
|
||||
const result: string[] = [];
|
||||
node.incomingLinks.forEach((names, parent) => {
|
||||
if (parent) {
|
||||
for (const path of this._getPaths(parent)) {
|
||||
names.forEach(name => result.push(vpath.combine(path, name)));
|
||||
}
|
||||
}
|
||||
else {
|
||||
names.forEach(name => result.push(name));
|
||||
}
|
||||
});
|
||||
node.paths = result;
|
||||
}
|
||||
return node.paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk a path to its end.
|
||||
*
|
||||
|
@ -952,6 +1019,176 @@ namespace vfs {
|
|||
}
|
||||
return typeof value === "string" || Buffer.isBuffer(value) ? new File(value) : new Directory(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch a path for changes.
|
||||
*/
|
||||
public watch(path: string, callback?: (eventType: string, filename: string) => void): FSWatcher;
|
||||
/**
|
||||
* Watch a path for changes.
|
||||
*/
|
||||
public watch(path: string, options?: { recursive?: boolean }, callback?: (eventType: string, filename: string) => void): FSWatcher;
|
||||
public watch(path: string, options?: { recursive?: boolean } | typeof callback, callback?: (eventType: string, filename: string) => void): FSWatcher {
|
||||
if (typeof options === "function") callback = options, options = undefined;
|
||||
if (options === undefined) options = {};
|
||||
path = this._resolve(path);
|
||||
const watcher = new FSWatcher(this);
|
||||
const realpath = this.realpathSync(path);
|
||||
const recursive = this._noRecursiveWatchers ? false : !!options.recursive;
|
||||
const watchers = this._lazy.watchers || (this._lazy.watchers = new collections.SortedMap(this.stringComparer));
|
||||
|
||||
let pathWatchers = watchers.get(realpath);
|
||||
if (!pathWatchers) {
|
||||
pathWatchers = new FSWatcherEntrySet(realpath);
|
||||
watchers.set(realpath, pathWatchers);
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-string-literal
|
||||
pathWatchers.add(watcher["_entry"] = { watcher, path, recursive, container: pathWatchers });
|
||||
return typeof callback === "function" ? watcher.on("change", callback) : watcher;
|
||||
}
|
||||
|
||||
private _removeWatcher(entry: FSWatcherEntry) {
|
||||
// tslint:disable-next-line:no-string-literal
|
||||
entry.watcher["_entry"] = undefined;
|
||||
entry.container.delete(entry);
|
||||
if (entry.container.size === 0) {
|
||||
const watchers = this._lazy.watchers;
|
||||
if (watchers && entry.container === watchers.get(entry.container.path)) {
|
||||
watchers.delete(entry.container.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _removeWatchers(parent: DirectoryInode | undefined, name: string) {
|
||||
if (!this._lazy.watchers) return;
|
||||
const paths = parent ? this._getPaths(parent).map(path => vpath.combine(path, name)) : [name];
|
||||
for (const path of paths) {
|
||||
const watchers = this._lazy.watchers.get(path);
|
||||
if (watchers) {
|
||||
watchers.forEach(watcher => this._removeWatcher(watcher));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _notifyChild(parent: DirectoryInode, eventType: "change" | "rename", name: string) {
|
||||
this._notify(parent, eventType, name, /*noExactMatch*/ false);
|
||||
}
|
||||
|
||||
private _notifySelf(node: Inode, eventType: "change" | "rename") {
|
||||
this._notify(node, eventType, /*childPath*/ undefined, /*noExactMatch*/ false);
|
||||
}
|
||||
|
||||
private _notifyAncestors(node: Inode, eventType: "change" | "rename") {
|
||||
this._notify(node, eventType, /*childPath*/ undefined, /*noExactMatch*/ true);
|
||||
}
|
||||
|
||||
private _notify(node: Inode, eventType: "change" | "rename", childPath: string | undefined, noExactMatch: boolean) {
|
||||
if (!this._lazy.watchers) return;
|
||||
for (const path of this._getPaths(node)) {
|
||||
const fullPath = childPath ? vpath.combine(path, childPath) : path;
|
||||
const dirname = vpath.dirname(fullPath);
|
||||
this._lazy.watchers.forEach((watchers, watchedPath) => {
|
||||
const exactMatch = !noExactMatch && this.stringComparer(watchedPath, fullPath) === 0;
|
||||
const nonRecursiveMatch = watchers.nonRecursiveCount > 0 && this.stringComparer(watchedPath, dirname) === 0;
|
||||
const recursiveMatch = watchers.recursiveCount > 0 && vpath.beneath(watchedPath, dirname, this.ignoreCase);
|
||||
if (exactMatch || nonRecursiveMatch || recursiveMatch) {
|
||||
watchers.forEach(({ watcher, recursive }) => {
|
||||
if (exactMatch || (recursive ? recursiveMatch : nonRecursiveMatch)) {
|
||||
// tslint:disable-next-line:no-string-literal
|
||||
const entry = watcher["_entry"];
|
||||
const name = exactMatch ? vpath.basename(entry ? entry.path : fullPath) : vpath.relative(watchedPath, fullPath, this.ignoreCase);
|
||||
watcher.emit("change", eventType, name);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch a path for changes using polling.
|
||||
*/
|
||||
public watchFile(path: string, callback: (current: Stats, previous: Stats) => void): void;
|
||||
/**
|
||||
* Watch a path for changes using polling.
|
||||
*/
|
||||
public watchFile(path: string, options: { interval?: number } | undefined, callback: (current: Stats, previous: Stats) => void): void;
|
||||
public watchFile(path: string, options: { interval?: number } | typeof callback | undefined, callback?: (current: Stats, previous: Stats) => void): void {
|
||||
if (typeof options === "function") callback = options, options = undefined;
|
||||
if (options === undefined) options = {};
|
||||
if (typeof callback !== "function") throw createIOError("EINVAL");
|
||||
path = this._resolve(path);
|
||||
const entry = this._walk(path, /*noFollow*/ undefined, () => "stop");
|
||||
const { interval = 5000 } = options;
|
||||
const watchedFiles = this._lazy.watchedFiles || (this._lazy.watchedFiles = new collections.SortedMap(this.stringComparer));
|
||||
let watchedFileSet = watchedFiles.get(path);
|
||||
if (!watchedFileSet) watchedFiles.set(path, watchedFileSet = new Set());
|
||||
const watchedFile: WatchedFile = {
|
||||
path,
|
||||
handle: this._timers.setInterval(() => this._onWatchInterval(watchedFile), interval),
|
||||
previous: entry ? this._stat(entry) : new Stats(),
|
||||
listener: callback
|
||||
};
|
||||
watchedFileSet.add(watchedFile);
|
||||
if (!entry) {
|
||||
callback(watchedFile.previous, watchedFile.previous);
|
||||
}
|
||||
}
|
||||
|
||||
private _onWatchInterval(watchedFile: WatchedFile) {
|
||||
if (watchedFile.handle === undefined) return;
|
||||
const entry = this._walk(watchedFile.path, /*noFollow*/ undefined, () => "stop");
|
||||
const previous = watchedFile.previous;
|
||||
const current = entry ? this._stat(entry) : new Stats();
|
||||
if (current.dev !== previous.dev ||
|
||||
current.ino !== previous.ino ||
|
||||
current.mode !== previous.mode ||
|
||||
current.nlink !== previous.nlink ||
|
||||
current.uid !== previous.uid ||
|
||||
current.gid !== previous.gid ||
|
||||
current.rdev !== previous.rdev ||
|
||||
current.size !== previous.size ||
|
||||
current.blksize !== previous.blksize ||
|
||||
current.blocks !== previous.blocks ||
|
||||
current.atimeMs !== previous.atimeMs ||
|
||||
current.mtimeMs !== previous.mtimeMs ||
|
||||
current.ctimeMs !== previous.ctimeMs ||
|
||||
current.birthtimeMs !== previous.birthtimeMs) {
|
||||
watchedFile.previous = current;
|
||||
const callback = watchedFile.listener;
|
||||
callback(current, previous);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop watching a path for changes.
|
||||
*/
|
||||
public unwatchFile(path: string, callback?: (current: Stats, previous: Stats) => void) {
|
||||
path = this._resolve(path);
|
||||
const watchedFiles = this._lazy.watchedFiles;
|
||||
if (!watchedFiles) return;
|
||||
|
||||
const watchedFileSet = watchedFiles.get(path);
|
||||
if (!watchedFileSet) return;
|
||||
|
||||
const watchedFilesToDelete: WatchedFile[] = [];
|
||||
watchedFileSet.forEach(watchedFile => {
|
||||
if (!callback || watchedFile.listener === callback) {
|
||||
this._timers.clearInterval(watchedFile.handle);
|
||||
watchedFilesToDelete.push(watchedFile);
|
||||
watchedFile.handle = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
for (const watchedFile of watchedFilesToDelete) {
|
||||
watchedFileSet.delete(watchedFile);
|
||||
}
|
||||
|
||||
if (watchedFileSet.size === 0) {
|
||||
watchedFiles.delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface FileSystemOptions {
|
||||
|
@ -967,6 +1204,9 @@ namespace vfs {
|
|||
|
||||
// Sets initial metadata attached to the file system.
|
||||
meta?: Record<string, any>;
|
||||
|
||||
timers?: FileSystemTimers;
|
||||
noRecursiveWatchers?: boolean;
|
||||
}
|
||||
|
||||
export interface FileSystemCreateOptions {
|
||||
|
@ -977,6 +1217,11 @@ namespace vfs {
|
|||
cwd?: string;
|
||||
}
|
||||
|
||||
export interface FileSystemTimers {
|
||||
setInterval(callback: (...args: any[]) => void, timeout: number, ...args: any[]): any;
|
||||
clearInterval(timeout: any): void;
|
||||
}
|
||||
|
||||
export type Axis = "ancestors" | "ancestors-or-self" | "self" | "descendants-or-self" | "descendants";
|
||||
|
||||
export interface Traversal {
|
||||
|
@ -1214,6 +1459,9 @@ namespace vfs {
|
|||
resolver?: FileSystemResolver;
|
||||
shadowRoot?: FileInode;
|
||||
meta?: collections.Metadata;
|
||||
paths?: ReadonlyArray<string>;
|
||||
pathsVersion?: number;
|
||||
incomingLinks: Map<DirectoryInode | undefined, collections.SortedSet<string>>;
|
||||
}
|
||||
|
||||
interface DirectoryInode {
|
||||
|
@ -1230,6 +1478,9 @@ namespace vfs {
|
|||
resolver?: FileSystemResolver;
|
||||
shadowRoot?: DirectoryInode;
|
||||
meta?: collections.Metadata;
|
||||
paths?: ReadonlyArray<string>;
|
||||
pathsVersion?: number;
|
||||
incomingLinks: Map<DirectoryInode | undefined, collections.SortedSet<string>>;
|
||||
}
|
||||
|
||||
interface SymlinkInode {
|
||||
|
@ -1244,6 +1495,9 @@ namespace vfs {
|
|||
symlink: string;
|
||||
shadowRoot?: SymlinkInode;
|
||||
meta?: collections.Metadata;
|
||||
paths?: ReadonlyArray<string>;
|
||||
pathsVersion?: number;
|
||||
incomingLinks: Map<DirectoryInode | undefined, collections.SortedSet<string>>;
|
||||
}
|
||||
|
||||
function isFile(node: Inode | undefined): node is FileInode {
|
||||
|
@ -1266,6 +1520,115 @@ namespace vfs {
|
|||
node: Inode | undefined;
|
||||
}
|
||||
|
||||
interface WatchedFile {
|
||||
path: string;
|
||||
handle: any;
|
||||
previous: Stats;
|
||||
listener: (current: Stats, previous: Stats) => void;
|
||||
}
|
||||
|
||||
interface FSWatcherEntry {
|
||||
watcher: FSWatcher;
|
||||
path: string;
|
||||
container: FSWatcherEntrySet;
|
||||
recursive: boolean;
|
||||
}
|
||||
|
||||
class FSWatcherEntrySet {
|
||||
public readonly path: string;
|
||||
private _recursiveCount = 0;
|
||||
private _nonRecursiveCount = 0;
|
||||
private _set = new Set<FSWatcherEntry>();
|
||||
constructor(path: string) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public get size() { return this._set.size; }
|
||||
public get recursiveCount() { return this._recursiveCount; }
|
||||
public get nonRecursiveCount() { return this._nonRecursiveCount; }
|
||||
|
||||
public add(entry: FSWatcherEntry) {
|
||||
const size = this.size;
|
||||
this._set.add(entry);
|
||||
if (this.size !== size) {
|
||||
if (entry.recursive) {
|
||||
this._recursiveCount++;
|
||||
}
|
||||
else {
|
||||
this._nonRecursiveCount++;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public delete(entry: FSWatcherEntry) {
|
||||
if (this._set.delete(entry)) {
|
||||
if (entry.recursive) {
|
||||
this._recursiveCount--;
|
||||
}
|
||||
else {
|
||||
this._nonRecursiveCount--;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this._recursiveCount = 0;
|
||||
this._nonRecursiveCount = 0;
|
||||
}
|
||||
|
||||
public forEach(callback: (value: FSWatcherEntry, key: FSWatcherEntry) => void) {
|
||||
this._set.forEach(callback);
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:variable-name
|
||||
const events = require("events") as typeof import("events");
|
||||
|
||||
class FSWatcher extends events.EventEmitter {
|
||||
private _fs: FileSystem;
|
||||
private _entry: FSWatcherEntry | undefined;
|
||||
|
||||
constructor(fs: FileSystem) {
|
||||
super();
|
||||
this._fs = fs;
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
if (this._entry) {
|
||||
// tslint:disable-next-line:no-string-literal
|
||||
this._fs["_removeWatcher"](this._entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #region FSWatcher Event "change"
|
||||
|
||||
interface FSWatcher {
|
||||
on(event: "change", listener: (eventType: string, filename: string) => void): this;
|
||||
once(event: "change", listener: (eventType: string, filename: string) => void): this;
|
||||
addListener(event: "change", listener: (eventType: string, filename: string) => void): this;
|
||||
removeListener(event: "change", listener: (eventType: string, filename: string) => void): this;
|
||||
prependListener(event: "change", listener: (eventType: string, filename: string) => void): this;
|
||||
prependOnceListener(event: "change", listener: (eventType: string, filename: string) => void): this;
|
||||
emit(name: "change", eventType: string, filename: string): boolean;
|
||||
}
|
||||
// #endregion FSWatcher Event "change"
|
||||
|
||||
// #region FSWatcher Event "error"
|
||||
interface FSWatcher {
|
||||
on(event: "error", listener: (error: Error) => void): this;
|
||||
once(event: "error", listener: (error: Error) => void): this;
|
||||
addListener(event: "error", listener: (error: Error) => void): this;
|
||||
removeListener(event: "error", listener: (error: Error) => void): this;
|
||||
prependListener(event: "error", listener: (error: Error) => void): this;
|
||||
prependOnceListener(event: "error", listener: (error: Error) => void): this;
|
||||
emit(name: "error", error: Error): boolean;
|
||||
}
|
||||
// #endregion FSWatcher Event "error"
|
||||
|
||||
let builtLocalHost: FileSystemResolverHost | undefined;
|
||||
let builtLocalCI: FileSystem | undefined;
|
||||
let builtLocalCS: FileSystem | undefined;
|
||||
|
|
Loading…
Reference in a new issue