File size: 2,993 Bytes
4cadbaf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142

import fs from "fs";
import {EventEmitter} from "events";
import sha1 from "sha1";
import * as diff from "diff";

import asyncCall from "./asyncCall";



export default class FileProxy extends EventEmitter {
	content: string;
	timestamp: number;
	diskTimestamp: number;
	filePath: string;

	alive: boolean = true;
	writeFile: () => void;

	fileListener: (curr: fs.Stats) => Promise<void>;


	constructor (filePath: string) {
		super();

		this.filePath = filePath;

		//console.log("File proxy created:", filePath);
		if (!fs.existsSync(filePath))
			throw new Error(`file not exist: ${filePath}`);

		asyncCall(fs.stat, filePath)
			.then(stats => {
				this.diskTimestamp = stats.mtime.getTime();
				this.timestamp = this.diskTimestamp;

				return asyncCall(fs.readFile, filePath);
			})
			.then(buffer => {
				this.content = buffer.toString();
				this.fullSync();
			});

		this.fileListener = async current => {
			this.diskTimestamp = current.mtime.getTime();
			this.timestamp = this.diskTimestamp;

			const buffer = await asyncCall(fs.readFile, filePath);
			if (!buffer) {
				this.emit("error", {description: "file reading failed"});
				return;
			}

			const newContent = buffer.toString();
			const newHash = sha1(newContent);
			if (newHash !== this.hash) {
				const patch = diff.createPatch(filePath, this.content, newContent);

				this.emit("increase", {
					timestamp: this.timestamp,
					fromHash: this.hash,
					toHash: newHash,
					patch,
				});
	
				this.content = newContent;
			}
		};

		fs.watchFile(filePath, this.fileListener);

		this.keepWriteFile();
	}


	dispose () {
		this.alive = false;

		if (this.fileListener)
			fs.unwatchFile(this.filePath, this.fileListener);
	}


	makeWritePromise (): Promise<void> {
		return new Promise(resolve => this.writeFile = resolve);
	}


	async keepWriteFile () {
		let writeSignal = this.makeWritePromise();

		while (this.alive) {
			await writeSignal;
			writeSignal = this.makeWritePromise();

			//console.debug("keepWriteFile:", this.timestamp, this.diskTimestamp);
			if (this.timestamp > this.diskTimestamp) {
				await asyncCall(fs.writeFile, this.filePath, this.content);
			}
		}
	}


	get hash (): string {
		return sha1(this.content);
	}


	fullSync () {
		this.emit("fullSync", {
			timestamp: this.timestamp,
			content: this.content,
			hash: this.hash,
		});
	}


	increase ({timestamp, fromHash, toHash, patch}: {
		timestamp: number,
		fromHash: string,
		toHash: string,
		patch: string,
	}) {
		if (this.hash !== fromHash) {
			if (this.timestamp > timestamp)
				// web content is out of date
				this.fullSync();
			else
				console.warn("[FileProxy] disk file content is behind increase base:", this.timestamp, timestamp);
		}
		else {
			this.content = diff.applyPatch(this.content, patch);
			this.timestamp = timestamp;

			console.assert(this.hash === toHash, "[FileProxy] verify failed:", this.hash, toHash, this.content);

			// trigger file writing
			this.writeFile();
		}
	}
};