1 | "use strict";
|
---|
2 | /**
|
---|
3 | * @license
|
---|
4 | * Copyright Google LLC All Rights Reserved.
|
---|
5 | *
|
---|
6 | * Use of this source code is governed by an MIT-style license that can be
|
---|
7 | * found in the LICENSE file at https://angular.io/license
|
---|
8 | */
|
---|
9 | Object.defineProperty(exports, "__esModule", { value: true });
|
---|
10 | exports.CordHost = void 0;
|
---|
11 | const rxjs_1 = require("rxjs");
|
---|
12 | const operators_1 = require("rxjs/operators");
|
---|
13 | const exception_1 = require("../../exception");
|
---|
14 | const memory_1 = require("./memory");
|
---|
15 | /**
|
---|
16 | * A Host that records changes to the underlying Host, while keeping a record of Create, Overwrite,
|
---|
17 | * Rename and Delete of files.
|
---|
18 | *
|
---|
19 | * This is fully compatible with Host, but will keep a staging of every changes asked. That staging
|
---|
20 | * follows the principle of the Tree (e.g. can create a file that already exists).
|
---|
21 | *
|
---|
22 | * Using `create()` and `overwrite()` will force those operations, but using `write` will add
|
---|
23 | * the create/overwrite records IIF the files does/doesn't already exist.
|
---|
24 | */
|
---|
25 | class CordHost extends memory_1.SimpleMemoryHost {
|
---|
26 | constructor(_back) {
|
---|
27 | super();
|
---|
28 | this._back = _back;
|
---|
29 | this._filesToCreate = new Set();
|
---|
30 | this._filesToRename = new Map();
|
---|
31 | this._filesToRenameRevert = new Map();
|
---|
32 | this._filesToDelete = new Set();
|
---|
33 | this._filesToOverwrite = new Set();
|
---|
34 | }
|
---|
35 | get backend() {
|
---|
36 | return this._back;
|
---|
37 | }
|
---|
38 | get capabilities() {
|
---|
39 | // Our own host is always Synchronous, but the backend might not be.
|
---|
40 | return {
|
---|
41 | synchronous: this._back.capabilities.synchronous,
|
---|
42 | };
|
---|
43 | }
|
---|
44 | /**
|
---|
45 | * Create a copy of this host, including all actions made.
|
---|
46 | * @returns {CordHost} The carbon copy.
|
---|
47 | */
|
---|
48 | clone() {
|
---|
49 | const dolly = new CordHost(this._back);
|
---|
50 | dolly._cache = new Map(this._cache);
|
---|
51 | dolly._filesToCreate = new Set(this._filesToCreate);
|
---|
52 | dolly._filesToRename = new Map(this._filesToRename);
|
---|
53 | dolly._filesToRenameRevert = new Map(this._filesToRenameRevert);
|
---|
54 | dolly._filesToDelete = new Set(this._filesToDelete);
|
---|
55 | dolly._filesToOverwrite = new Set(this._filesToOverwrite);
|
---|
56 | return dolly;
|
---|
57 | }
|
---|
58 | /**
|
---|
59 | * Commit the changes recorded to a Host. It is assumed that the host does have the same structure
|
---|
60 | * as the host that was used for backend (could be the same host).
|
---|
61 | * @param host The host to create/delete/rename/overwrite files to.
|
---|
62 | * @param force Whether to skip existence checks when creating/overwriting. This is
|
---|
63 | * faster but might lead to incorrect states. Because Hosts natively don't support creation
|
---|
64 | * versus overwriting (it's only writing), we check for existence before completing a request.
|
---|
65 | * @returns An observable that completes when done, or error if an error occured.
|
---|
66 | */
|
---|
67 | commit(host, force = false) {
|
---|
68 | // Really commit everything to the actual host.
|
---|
69 | return rxjs_1.from(this.records()).pipe(operators_1.concatMap((record) => {
|
---|
70 | switch (record.kind) {
|
---|
71 | case 'delete':
|
---|
72 | return host.delete(record.path);
|
---|
73 | case 'rename':
|
---|
74 | return host.rename(record.from, record.to);
|
---|
75 | case 'create':
|
---|
76 | return host.exists(record.path).pipe(operators_1.switchMap((exists) => {
|
---|
77 | if (exists && !force) {
|
---|
78 | return rxjs_1.throwError(new exception_1.FileAlreadyExistException(record.path));
|
---|
79 | }
|
---|
80 | else {
|
---|
81 | return host.write(record.path, record.content);
|
---|
82 | }
|
---|
83 | }));
|
---|
84 | case 'overwrite':
|
---|
85 | return host.exists(record.path).pipe(operators_1.switchMap((exists) => {
|
---|
86 | if (!exists && !force) {
|
---|
87 | return rxjs_1.throwError(new exception_1.FileDoesNotExistException(record.path));
|
---|
88 | }
|
---|
89 | else {
|
---|
90 | return host.write(record.path, record.content);
|
---|
91 | }
|
---|
92 | }));
|
---|
93 | }
|
---|
94 | }), operators_1.reduce(() => { }));
|
---|
95 | }
|
---|
96 | records() {
|
---|
97 | return [
|
---|
98 | ...[...this._filesToDelete.values()].map((path) => ({
|
---|
99 | kind: 'delete',
|
---|
100 | path,
|
---|
101 | })),
|
---|
102 | ...[...this._filesToRename.entries()].map(([from, to]) => ({
|
---|
103 | kind: 'rename',
|
---|
104 | from,
|
---|
105 | to,
|
---|
106 | })),
|
---|
107 | ...[...this._filesToCreate.values()].map((path) => ({
|
---|
108 | kind: 'create',
|
---|
109 | path,
|
---|
110 | content: this._read(path),
|
---|
111 | })),
|
---|
112 | ...[...this._filesToOverwrite.values()].map((path) => ({
|
---|
113 | kind: 'overwrite',
|
---|
114 | path,
|
---|
115 | content: this._read(path),
|
---|
116 | })),
|
---|
117 | ];
|
---|
118 | }
|
---|
119 | /**
|
---|
120 | * Specialized version of {@link CordHost#write} which forces the creation of a file whether it
|
---|
121 | * exists or not.
|
---|
122 | * @param {} path
|
---|
123 | * @param {FileBuffer} content
|
---|
124 | * @returns {Observable<void>}
|
---|
125 | */
|
---|
126 | create(path, content) {
|
---|
127 | if (super._exists(path)) {
|
---|
128 | throw new exception_1.FileAlreadyExistException(path);
|
---|
129 | }
|
---|
130 | if (this._filesToDelete.has(path)) {
|
---|
131 | this._filesToDelete.delete(path);
|
---|
132 | this._filesToOverwrite.add(path);
|
---|
133 | }
|
---|
134 | else {
|
---|
135 | this._filesToCreate.add(path);
|
---|
136 | }
|
---|
137 | return super.write(path, content);
|
---|
138 | }
|
---|
139 | overwrite(path, content) {
|
---|
140 | return this.isDirectory(path).pipe(operators_1.switchMap((isDir) => {
|
---|
141 | if (isDir) {
|
---|
142 | return rxjs_1.throwError(new exception_1.PathIsDirectoryException(path));
|
---|
143 | }
|
---|
144 | return this.exists(path);
|
---|
145 | }), operators_1.switchMap((exists) => {
|
---|
146 | if (!exists) {
|
---|
147 | return rxjs_1.throwError(new exception_1.FileDoesNotExistException(path));
|
---|
148 | }
|
---|
149 | if (!this._filesToCreate.has(path)) {
|
---|
150 | this._filesToOverwrite.add(path);
|
---|
151 | }
|
---|
152 | return super.write(path, content);
|
---|
153 | }));
|
---|
154 | }
|
---|
155 | write(path, content) {
|
---|
156 | return this.exists(path).pipe(operators_1.switchMap((exists) => {
|
---|
157 | if (exists) {
|
---|
158 | // It exists, but might be being renamed or deleted. In that case we want to create it.
|
---|
159 | if (this.willRename(path) || this.willDelete(path)) {
|
---|
160 | return this.create(path, content);
|
---|
161 | }
|
---|
162 | else {
|
---|
163 | return this.overwrite(path, content);
|
---|
164 | }
|
---|
165 | }
|
---|
166 | else {
|
---|
167 | return this.create(path, content);
|
---|
168 | }
|
---|
169 | }));
|
---|
170 | }
|
---|
171 | read(path) {
|
---|
172 | if (this._exists(path)) {
|
---|
173 | return super.read(path);
|
---|
174 | }
|
---|
175 | return this._back.read(path);
|
---|
176 | }
|
---|
177 | delete(path) {
|
---|
178 | if (this._exists(path)) {
|
---|
179 | if (this._filesToCreate.has(path)) {
|
---|
180 | this._filesToCreate.delete(path);
|
---|
181 | }
|
---|
182 | else if (this._filesToOverwrite.has(path)) {
|
---|
183 | this._filesToOverwrite.delete(path);
|
---|
184 | this._filesToDelete.add(path);
|
---|
185 | }
|
---|
186 | else {
|
---|
187 | const maybeOrigin = this._filesToRenameRevert.get(path);
|
---|
188 | if (maybeOrigin) {
|
---|
189 | this._filesToRenameRevert.delete(path);
|
---|
190 | this._filesToRename.delete(maybeOrigin);
|
---|
191 | this._filesToDelete.add(maybeOrigin);
|
---|
192 | }
|
---|
193 | else {
|
---|
194 | return rxjs_1.throwError(new exception_1.UnknownException(`This should never happen. Path: ${JSON.stringify(path)}.`));
|
---|
195 | }
|
---|
196 | }
|
---|
197 | return super.delete(path);
|
---|
198 | }
|
---|
199 | else {
|
---|
200 | return this._back.exists(path).pipe(operators_1.switchMap((exists) => {
|
---|
201 | if (exists) {
|
---|
202 | this._filesToDelete.add(path);
|
---|
203 | return rxjs_1.of();
|
---|
204 | }
|
---|
205 | else {
|
---|
206 | return rxjs_1.throwError(new exception_1.FileDoesNotExistException(path));
|
---|
207 | }
|
---|
208 | }));
|
---|
209 | }
|
---|
210 | }
|
---|
211 | rename(from, to) {
|
---|
212 | return rxjs_1.concat(this.exists(to), this.exists(from)).pipe(operators_1.toArray(), operators_1.switchMap(([existTo, existFrom]) => {
|
---|
213 | if (!existFrom) {
|
---|
214 | return rxjs_1.throwError(new exception_1.FileDoesNotExistException(from));
|
---|
215 | }
|
---|
216 | if (from === to) {
|
---|
217 | return rxjs_1.EMPTY;
|
---|
218 | }
|
---|
219 | if (existTo) {
|
---|
220 | return rxjs_1.throwError(new exception_1.FileAlreadyExistException(to));
|
---|
221 | }
|
---|
222 | // If we're renaming a file that's been created, shortcircuit to creating the `to` path.
|
---|
223 | if (this._filesToCreate.has(from)) {
|
---|
224 | this._filesToCreate.delete(from);
|
---|
225 | this._filesToCreate.add(to);
|
---|
226 | return super.rename(from, to);
|
---|
227 | }
|
---|
228 | if (this._filesToOverwrite.has(from)) {
|
---|
229 | this._filesToOverwrite.delete(from);
|
---|
230 | // Recursively call this function. This is so we don't repeat the bottom logic. This
|
---|
231 | // if will be by-passed because we just deleted the `from` path from files to overwrite.
|
---|
232 | return rxjs_1.concat(this.rename(from, to), new rxjs_1.Observable((x) => {
|
---|
233 | this._filesToOverwrite.add(to);
|
---|
234 | x.complete();
|
---|
235 | }));
|
---|
236 | }
|
---|
237 | if (this._filesToDelete.has(to)) {
|
---|
238 | this._filesToDelete.delete(to);
|
---|
239 | this._filesToDelete.add(from);
|
---|
240 | this._filesToOverwrite.add(to);
|
---|
241 | // We need to delete the original and write the new one.
|
---|
242 | return this.read(from).pipe(operators_1.map((content) => this._write(to, content)));
|
---|
243 | }
|
---|
244 | const maybeTo1 = this._filesToRenameRevert.get(from);
|
---|
245 | if (maybeTo1) {
|
---|
246 | // We already renamed to this file (A => from), let's rename the former to the new
|
---|
247 | // path (A => to).
|
---|
248 | this._filesToRename.delete(maybeTo1);
|
---|
249 | this._filesToRenameRevert.delete(from);
|
---|
250 | from = maybeTo1;
|
---|
251 | }
|
---|
252 | this._filesToRename.set(from, to);
|
---|
253 | this._filesToRenameRevert.set(to, from);
|
---|
254 | // If the file is part of our data, just rename it internally.
|
---|
255 | if (this._exists(from)) {
|
---|
256 | return super.rename(from, to);
|
---|
257 | }
|
---|
258 | else {
|
---|
259 | // Create a file with the same content.
|
---|
260 | return this._back.read(from).pipe(operators_1.switchMap((content) => super.write(to, content)));
|
---|
261 | }
|
---|
262 | }));
|
---|
263 | }
|
---|
264 | list(path) {
|
---|
265 | return rxjs_1.concat(super.list(path), this._back.list(path)).pipe(operators_1.reduce((list, curr) => {
|
---|
266 | curr.forEach((elem) => list.add(elem));
|
---|
267 | return list;
|
---|
268 | }, new Set()), operators_1.map((set) => [...set]));
|
---|
269 | }
|
---|
270 | exists(path) {
|
---|
271 | return this._exists(path)
|
---|
272 | ? rxjs_1.of(true)
|
---|
273 | : this.willDelete(path) || this.willRename(path)
|
---|
274 | ? rxjs_1.of(false)
|
---|
275 | : this._back.exists(path);
|
---|
276 | }
|
---|
277 | isDirectory(path) {
|
---|
278 | return this._exists(path) ? super.isDirectory(path) : this._back.isDirectory(path);
|
---|
279 | }
|
---|
280 | isFile(path) {
|
---|
281 | return this._exists(path)
|
---|
282 | ? super.isFile(path)
|
---|
283 | : this.willDelete(path) || this.willRename(path)
|
---|
284 | ? rxjs_1.of(false)
|
---|
285 | : this._back.isFile(path);
|
---|
286 | }
|
---|
287 | stat(path) {
|
---|
288 | return this._exists(path)
|
---|
289 | ? super.stat(path)
|
---|
290 | : this.willDelete(path) || this.willRename(path)
|
---|
291 | ? rxjs_1.of(null)
|
---|
292 | : this._back.stat(path);
|
---|
293 | }
|
---|
294 | watch(path, options) {
|
---|
295 | // Watching not supported.
|
---|
296 | return null;
|
---|
297 | }
|
---|
298 | willCreate(path) {
|
---|
299 | return this._filesToCreate.has(path);
|
---|
300 | }
|
---|
301 | willOverwrite(path) {
|
---|
302 | return this._filesToOverwrite.has(path);
|
---|
303 | }
|
---|
304 | willDelete(path) {
|
---|
305 | return this._filesToDelete.has(path);
|
---|
306 | }
|
---|
307 | willRename(path) {
|
---|
308 | return this._filesToRename.has(path);
|
---|
309 | }
|
---|
310 | willRenameTo(path, to) {
|
---|
311 | return this._filesToRename.get(path) === to;
|
---|
312 | }
|
---|
313 | }
|
---|
314 | exports.CordHost = CordHost;
|
---|