UNPKG

8.53 kBPlain TextView Raw
1import {Correlation, FileSystem, isDisposable, UnexpectedErrorEvent} from './api';
2import {Directory, DirectoryContent, File, FileSystemNode, isDir, isFile, ShallowDirectory} from "./model";
3
4import {MemoryFileSystem} from './memory-fs';
5import {InternalEventsEmitter, makeEventsEmitter} from './utils';
6import {FileSystemReadSync} from './browser';
7
8type PathInCache = {
9 [prop: string]: boolean
10};
11
12interface FileSystemNodesMap {
13 [key: string]: FileSystemNode
14}
15
16interface TreesDiff {
17 toAdd: FileSystemNode[],
18 toDelete: FileSystemNode[],
19 toChange: string[]
20}
21
22function nodesToMap(tree: FileSystemNode[], accumulator: FileSystemNodesMap = {}): FileSystemNodesMap {
23 tree.forEach(node => {
24 if (isDir(node)) nodesToMap(node.children, accumulator);
25 accumulator[node.fullPath] = node;
26 })
27
28 return accumulator;
29};
30
31function getTreesDiff(cached: FileSystemNodesMap, real: FileSystemNodesMap): TreesDiff {
32 const diff: TreesDiff = {
33 toAdd: [],
34 toDelete: [],
35 toChange: []
36 };
37
38 Object.keys(cached).forEach(cachedPath => {
39 if (!real[cachedPath]) {
40 diff.toDelete.push(cached[cachedPath]);
41 diff.toChange = diff.toChange.filter(path => path !== cachedPath);
42 } else {
43 const node = cached[cachedPath];
44 if (isFile(node) && node.content) diff.toChange.push(cachedPath);
45 }
46 });
47
48 Object.keys(real).forEach(realPath => {
49 if (!cached[realPath]) diff.toAdd.push(real[realPath]);
50 });
51
52 return diff;
53}
54
55export class CacheFileSystem implements FileSystemReadSync, FileSystem {
56 public readonly events: InternalEventsEmitter = makeEventsEmitter();
57
58 public baseUrl: string;
59 private cache: MemoryFileSystem;
60 private isTreeCached: boolean = false;
61 private pathsInCache: PathInCache = {};
62 private onFsError = ({stack}: Error | UnexpectedErrorEvent) => {
63 this.shouldRescanOnError ?
64 this.rescanOnError() :
65 this.emit('unexpectedError', {stack});
66 };
67
68 constructor(private fs: FileSystem, private shouldRescanOnError: boolean = true) {
69 this.baseUrl = fs.baseUrl;
70 this.cache = new MemoryFileSystem();
71
72 this.fs.events.on('unexpectedError', this.onFsError);
73
74 this.fs.events.on('fileCreated', event => {
75 const {fullPath, newContent} = event;
76
77 try {
78 this.cache.saveFileSync(fullPath, newContent);
79 this.pathsInCache[fullPath] = true;
80 this.events.emit('fileCreated', event)
81 } catch (e) {
82 this.onFsError(e)
83 }
84 });
85
86 this.fs.events.on('fileChanged', event => {
87 const {fullPath, newContent} = event;
88 try {
89 this.cache.saveFileSync(fullPath, newContent);
90 this.pathsInCache[fullPath] = true;
91 this.events.emit('fileChanged', event);
92 } catch (e) {
93 this.onFsError(e)
94 }
95 });
96
97 this.fs.events.on('fileDeleted', event => {
98 try {
99 this.cache.deleteFileSync(event.fullPath);
100 this.pathsInCache[event.fullPath] = true;
101 this.events.emit('fileDeleted', event);
102 } catch (e) {
103 this.onFsError(e)
104 }
105 });
106
107 this.fs.events.on('directoryCreated', event => {
108 try {
109 this.cache.ensureDirectorySync(event.fullPath);
110 this.events.emit('directoryCreated', event);
111 } catch (e) {
112 this.onFsError(e)
113 }
114 });
115
116 this.fs.events.on('directoryDeleted', event => {
117 try {
118 this.cache.deleteDirectorySync(event.fullPath, true);
119 this.events.emit('directoryDeleted', event);
120 } catch (e) {
121 this.onFsError(e)
122 }
123 });
124 }
125
126 async saveFile(fullPath: string, newContent: string): Promise<Correlation> {
127 const correlation = await this.fs.saveFile(fullPath, newContent);
128 this.cache.saveFileSync(fullPath, newContent);
129 this.pathsInCache[fullPath] = true;
130 return correlation;
131 }
132
133 async deleteFile(fullPath: string): Promise<Correlation> {
134 const correlation = await this.fs.deleteFile(fullPath);
135 this.cache.deleteFileSync(fullPath);
136 return correlation;
137 }
138
139 async deleteDirectory(fullPath: string, recursive: boolean = false): Promise<Correlation> {
140 const correlation = await this.fs.deleteDirectory(fullPath, recursive);
141 this.cache.deleteDirectorySync(fullPath, recursive);
142 return correlation;
143 }
144
145 async ensureDirectory(fullPath: string): Promise<Correlation> {
146 const correlation = await this.fs.ensureDirectory(fullPath);
147 this.cache.ensureDirectorySync(fullPath);
148 return correlation;
149 }
150
151 async loadTextFile(fullPath: string): Promise<string> {
152 if (this.pathsInCache[fullPath]) {
153 return this.cache.loadTextFileSync(fullPath);
154 }
155
156 const file = await this.fs.loadTextFile(fullPath);
157 this.cache.saveFileSync(fullPath, file);
158 return this.cache.loadTextFileSync(fullPath);
159 }
160
161 loadTextFileSync(fullPath: string): string {
162 return this.cache.loadTextFileSync(fullPath);
163 }
164
165
166 async loadDirectoryTree(fullPath?: string): Promise<Directory> {
167 if (this.isTreeCached) {
168 return this.cache.loadDirectoryTreeSync(fullPath);
169 }
170
171 await this.cacheTree();
172 this.isTreeCached = true;
173 return this.cache.loadDirectoryTreeSync(fullPath);
174 }
175
176 loadDirectoryTreeSync(fullPath: string): Directory {
177 return this.cache.loadDirectoryTreeSync(fullPath);
178 }
179
180 loadDirectoryContentSync(fullPath: string = ''): DirectoryContent {
181 return this.cache.loadDirectoryContentSync(fullPath);
182 }
183
184 async loadDirectoryChildren(fullPath: string): Promise<(File | ShallowDirectory)[]> {
185 if (this.isTreeCached) {
186 return this.cache.loadDirectoryChildrenSync(fullPath);
187 }
188
189 await this.cacheTree();
190 this.isTreeCached = true;
191 return this.cache.loadDirectoryChildrenSync(fullPath);
192 }
193
194 loadDirectoryChildrenSync(fullPath: string): Array<File | ShallowDirectory> {
195 return this.cache.loadDirectoryChildrenSync(fullPath);
196 }
197
198
199 dispose() {
200 if (isDisposable(this.fs)) this.fs.dispose();
201 }
202
203 private rescanOnError() {
204 const cachedTree = this.cache.loadDirectoryTreeSync();
205 this.isTreeCached = false;
206
207 this.loadDirectoryTree().then(realTree => {
208 const {toDelete, toAdd, toChange} = getTreesDiff(
209 nodesToMap(cachedTree.children),
210 nodesToMap(realTree.children)
211 );
212
213 toDelete.forEach(node => {
214 this.emit(
215 `${isDir(node) ? 'directory' : 'file'}Deleted`,
216 {fullPath: node.fullPath}
217 );
218 });
219
220 toAdd.forEach(node => {
221 if (isDir(node)) {
222 this.emit(
223 'directoryCreated',
224 {fullPath: node.fullPath}
225 );
226 } else {
227 this.loadTextFile(node.fullPath).then(newContent => {
228 this.emit('fileCreated', {
229 fullPath: node.fullPath,
230 newContent
231 });
232 });
233 }
234 });
235
236 toChange.forEach(fullPath => this.loadTextFile(fullPath).then(newContent => {
237 this.emit('fileChanged', {fullPath, newContent});
238 }))
239 })
240 }
241
242 private emit(type: string, data: object) {
243 this.events.emit(type, {...data, type});
244 }
245
246 private async cacheTree(): Promise<FileSystem> {
247 this.cache = new MemoryFileSystem();
248 this.pathsInCache = {};
249 const tree = await this.fs.loadDirectoryTree();
250
251 return this.fill(tree);
252 }
253
254 private async fill(tree: FileSystemNode): Promise<FileSystem> {
255 if (isDir(tree)) {
256 this.cache.ensureDirectorySync(tree.fullPath);
257 await Promise.all(tree.children.map(child => this.fill(child)));
258 return this.cache;
259 }
260
261 this.cache.saveFileSync(tree.fullPath, '');
262 return this.cache;
263 }
264}