UNPKG

12.2 kBJavaScriptView Raw
1// @flow strict-local
2
3import type {AbortSignal} from 'abortcontroller-polyfill/dist/cjs-ponyfill';
4import type {File, FilePath, Glob} from '@parcel/types';
5import type {Event} from '@parcel/watcher';
6import type {NodeId} from './types';
7
8import invariant from 'assert';
9import nullthrows from 'nullthrows';
10import {isGlobMatch, md5FromObject} from '@parcel/utils';
11import Graph, {type GraphOpts} from './Graph';
12import {assertSignalNotAborted} from './utils';
13
14type SerializedRequestGraph = {|
15 ...GraphOpts<RequestGraphNode, RequestGraphEdgeType>,
16 invalidNodeIds: Set<NodeId>,
17 incompleteNodeIds: Set<NodeId>,
18 globNodeIds: Set<NodeId>,
19 unpredicatableNodeIds: Set<NodeId>,
20|};
21
22type FileNode = {|id: string, +type: 'file', value: File|};
23type GlobNode = {|id: string, +type: 'glob', value: Glob|};
24export type Request = {|
25 id: string,
26 +type: string,
27 request: mixed,
28 result?: mixed,
29|};
30
31type RequestNode = {|
32 id: string,
33 +type: 'request',
34 value: Request,
35|};
36type RequestGraphNode = RequestNode | FileNode | GlobNode;
37
38type RequestGraphEdgeType =
39 | 'subrequest'
40 | 'invalidated_by_update'
41 | 'invalidated_by_delete'
42 | 'invalidated_by_create';
43
44const nodeFromFilePath = (filePath: string) => ({
45 id: filePath,
46 type: 'file',
47 value: {filePath},
48});
49
50const nodeFromGlob = (glob: Glob) => ({
51 id: glob,
52 type: 'glob',
53 value: glob,
54});
55
56const nodeFromRequest = (request: Request) => ({
57 id: request.id,
58 type: 'request',
59 value: request,
60});
61
62export class RequestGraph extends Graph<
63 RequestGraphNode,
64 RequestGraphEdgeType,
65> {
66 invalidNodeIds: Set<NodeId> = new Set();
67 incompleteNodeIds: Set<NodeId> = new Set();
68 globNodeIds: Set<NodeId> = new Set();
69 // Unpredictable nodes are requests that cannot be predicted whether they should rerun based on
70 // filesystem changes alone. They should rerun on each startup of Parcel.
71 unpredicatableNodeIds: Set<NodeId> = new Set();
72
73 // $FlowFixMe
74 static deserialize(opts: SerializedRequestGraph) {
75 let deserialized = new RequestGraph(opts);
76 deserialized.invalidNodeIds = opts.invalidNodeIds;
77 deserialized.incompleteNodeIds = opts.incompleteNodeIds;
78 deserialized.globNodeIds = opts.globNodeIds;
79 deserialized.unpredicatableNodeIds = opts.unpredicatableNodeIds;
80 // $FlowFixMe
81 return deserialized;
82 }
83
84 // $FlowFixMe
85 serialize(): SerializedRequestGraph {
86 return {
87 ...super.serialize(),
88 invalidNodeIds: this.invalidNodeIds,
89 incompleteNodeIds: this.incompleteNodeIds,
90 globNodeIds: this.globNodeIds,
91 unpredicatableNodeIds: this.unpredicatableNodeIds,
92 };
93 }
94
95 addNode(node: RequestGraphNode) {
96 if (!this.hasNode(node.id)) {
97 if (node.type === 'glob') {
98 this.globNodeIds.add(node.id);
99 }
100 }
101
102 return super.addNode(node);
103 }
104
105 removeNode(node: RequestGraphNode) {
106 this.invalidNodeIds.delete(node.id);
107 this.incompleteNodeIds.delete(node.id);
108 if (node.type === 'glob') {
109 this.globNodeIds.delete(node.id);
110 }
111 return super.removeNode(node);
112 }
113
114 // TODO: deprecate
115 addRequest(request: Request) {
116 let requestNode = nodeFromRequest(request);
117 if (!this.hasNode(requestNode.id)) {
118 this.addNode(requestNode);
119 } else {
120 requestNode = this.getNode(requestNode.id);
121 }
122 return requestNode;
123 }
124
125 getRequestNode(id: string) {
126 let node = nullthrows(this.getNode(id));
127 invariant(node.type === 'request');
128 return node;
129 }
130
131 completeRequest(request: Request) {
132 this.invalidNodeIds.delete(request.id);
133 this.incompleteNodeIds.delete(request.id);
134 }
135
136 replaceSubrequests(
137 requestId: string,
138 subrequestNodes: Array<RequestGraphNode>,
139 ) {
140 let requestNode = this.getRequestNode(requestId);
141 if (!this.hasNode(requestId)) {
142 this.addNode(requestNode);
143 }
144
145 for (let subrequestNode of subrequestNodes) {
146 this.invalidNodeIds.delete(subrequestNode.id);
147 }
148
149 this.replaceNodesConnectedTo(
150 requestNode,
151 subrequestNodes,
152 null,
153 'subrequest',
154 );
155 }
156
157 invalidateNode(node: RequestGraphNode) {
158 invariant(node.type === 'request');
159 if (this.hasNode(node.id)) {
160 this.invalidNodeIds.add(node.id);
161 this.clearInvalidations(node);
162
163 let parentNodes = this.getNodesConnectedTo(node, 'subrequest');
164 for (let parentNode of parentNodes) {
165 this.invalidateNode(parentNode);
166 }
167 }
168 }
169
170 invalidateUnpredictableNodes() {
171 for (let nodeId of this.unpredicatableNodeIds) {
172 let node = nullthrows(this.getNode(nodeId));
173 invariant(node.type !== 'file' && node.type !== 'glob');
174 this.invalidateNode(node);
175 }
176 }
177
178 invalidateOnFileUpdate(requestId: string, filePath: FilePath) {
179 let requestNode = this.getRequestNode(requestId);
180 let fileNode = nodeFromFilePath(filePath);
181 if (!this.hasNode(fileNode.id)) {
182 this.addNode(fileNode);
183 }
184
185 if (!this.hasEdge(requestNode.id, fileNode.id, 'invalidated_by_update')) {
186 this.addEdge(requestNode.id, fileNode.id, 'invalidated_by_update');
187 }
188 }
189
190 invalidateOnFileDelete(requestId: string, filePath: FilePath) {
191 let requestNode = this.getRequestNode(requestId);
192 let fileNode = nodeFromFilePath(filePath);
193 if (!this.hasNode(fileNode.id)) {
194 this.addNode(fileNode);
195 }
196
197 if (!this.hasEdge(requestNode.id, fileNode.id, 'invalidated_by_delete')) {
198 this.addEdge(requestNode.id, fileNode.id, 'invalidated_by_delete');
199 }
200 }
201
202 invalidateOnFileCreate(requestId: string, glob: Glob) {
203 let requestNode = this.getRequestNode(requestId);
204 let globNode = nodeFromGlob(glob);
205 if (!this.hasNode(globNode.id)) {
206 this.addNode(globNode);
207 }
208
209 if (!this.hasEdge(requestNode.id, globNode.id, 'invalidated_by_create')) {
210 this.addEdge(requestNode.id, globNode.id, 'invalidated_by_create');
211 }
212 }
213
214 invalidateOnStartup(requestId: string) {
215 let requestNode = this.getRequestNode(requestId);
216 this.unpredicatableNodeIds.add(requestNode.id);
217 }
218
219 clearInvalidations(node: RequestNode) {
220 this.unpredicatableNodeIds.delete(node.id);
221 this.replaceNodesConnectedTo(node, [], null, 'invalidated_by_update');
222 this.replaceNodesConnectedTo(node, [], null, 'invalidated_by_delete');
223 this.replaceNodesConnectedTo(node, [], null, 'invalidated_by_create');
224 }
225
226 respondToFSEvents(events: Array<Event>): boolean {
227 let isInvalid = false;
228
229 for (let {path, type} of events) {
230 let node = this.getNode(path);
231
232 // sometimes mac os reports update events as create events
233 // if it was a create event, but the file already exists in the graph,
234 // then we can assume it was actually an update event
235 if (node && (type === 'create' || type === 'update')) {
236 for (let connectedNode of this.getNodesConnectedTo(
237 node,
238 'invalidated_by_update',
239 )) {
240 this.invalidateNode(connectedNode);
241 isInvalid = true;
242 }
243 } else if (type === 'create') {
244 for (let id of this.globNodeIds) {
245 let globNode = this.getNode(id);
246 invariant(globNode && globNode.type === 'glob');
247
248 if (isGlobMatch(path, globNode.value)) {
249 let connectedNodes = this.getNodesConnectedTo(
250 globNode,
251 'invalidated_by_create',
252 );
253 for (let connectedNode of connectedNodes) {
254 this.invalidateNode(connectedNode);
255 isInvalid = true;
256 }
257 }
258 }
259 } else if (node && type === 'delete') {
260 for (let connectedNode of this.getNodesConnectedTo(
261 node,
262 'invalidated_by_delete',
263 )) {
264 this.invalidateNode(connectedNode);
265 isInvalid = true;
266 }
267 }
268 }
269
270 return isInvalid;
271 }
272}
273
274export default class RequestTracker {
275 graph: RequestGraph;
276
277 constructor({graph}: {|graph: RequestGraph|}) {
278 this.graph = graph || new RequestGraph();
279 }
280
281 isTracked(id: string) {
282 return this.graph.hasNode(id);
283 }
284
285 getRequest(id: string) {
286 return nullthrows(this.graph.getNode(id));
287 }
288
289 trackRequest(request: Request) {
290 if (this.isTracked(request.id)) {
291 return;
292 }
293
294 this.graph.incompleteNodeIds.add(request.id);
295 let node = nodeFromRequest(request);
296 this.graph.addNode(node);
297 }
298
299 untrackRequest(id: string) {
300 this.graph.removeById(id);
301 }
302
303 hasValidResult(id: string) {
304 return (
305 !this.graph.invalidNodeIds.has(id) &&
306 !this.graph.incompleteNodeIds.has(id)
307 );
308 }
309
310 getRequestResult(id: string) {
311 let node = nullthrows(this.graph.getNode(id));
312 invariant(node.type === 'request');
313 return node.value.result;
314 }
315
316 completeRequest(id: string) {
317 this.graph.invalidNodeIds.delete(id);
318 this.graph.incompleteNodeIds.delete(id);
319 }
320
321 respondToFSEvents(events: Array<Event>): boolean {
322 return this.graph.respondToFSEvents(events);
323 }
324
325 hasInvalidRequests() {
326 return this.graph.invalidNodeIds.size > 0;
327 }
328
329 getInvalidRequests(): Array<Request> {
330 let invalidRequests = [];
331 for (let id of this.graph.invalidNodeIds) {
332 let node = nullthrows(this.graph.getNode(id));
333 invariant(node.type === 'request');
334 invalidRequests.push(node.value);
335 }
336 return invalidRequests;
337 }
338
339 replaceSubrequests(
340 requestId: string,
341 subrequestNodes: Array<RequestGraphNode>,
342 ) {
343 this.graph.replaceSubrequests(requestId, subrequestNodes);
344 }
345}
346
347type RequestRunnerOpts = {
348 tracker: RequestTracker,
349 ...
350};
351
352export type RunRequestOpts = {|
353 signal?: ?AbortSignal,
354 parentId?: string,
355|};
356
357export type RequestRunnerAPI = {|
358 invalidateOnFileCreate: Glob => void,
359 invalidateOnFileDelete: FilePath => void,
360 invalidateOnFileUpdate: FilePath => void,
361 invalidateOnStartup: () => void,
362 replaceSubrequests: (Array<RequestGraphNode>) => void,
363|};
364
365export function generateRequestId(type: string, request: mixed) {
366 return md5FromObject({type, request});
367}
368
369export class RequestRunner<TRequest, TResult> {
370 type: string;
371 tracker: RequestTracker;
372
373 constructor({tracker}: RequestRunnerOpts) {
374 this.tracker = tracker;
375 }
376
377 async runRequest(
378 requestDesc: TRequest,
379 {signal}: RunRequestOpts = {},
380 ): Promise<TResult | void> {
381 let id = this.generateRequestId(requestDesc);
382 let request = {id, type: this.type, request: requestDesc};
383
384 let api = this.createAPI(id);
385
386 this.tracker.trackRequest(request);
387 let result: TResult = this.tracker.hasValidResult(id)
388 ? // $FlowFixMe
389 (this.tracker.getRequestResult(id): any)
390 : await this.run(requestDesc, api);
391 assertSignalNotAborted(signal);
392 // Request may have been removed by a parent request
393 if (!this.tracker.isTracked(id)) {
394 return;
395 }
396 await this.onComplete(requestDesc, result, api);
397 this.tracker.completeRequest(id);
398
399 return result;
400 }
401
402 // unused vars are used for types
403 // eslint-disable-next-line no-unused-vars
404 run(request: TRequest, api: RequestRunnerAPI): Promise<TResult> {
405 throw new Error(
406 `RequestRunner for type ${this.type} did not implement run()`,
407 );
408 }
409
410 // unused vars are used for types
411 // eslint-disable-next-line no-unused-vars
412 onComplete(request: TRequest, result: TResult, api: RequestRunnerAPI) {
413 throw new Error(
414 `RequestRunner for type ${this.type} did not implement onComplete()`,
415 );
416 }
417
418 generateRequestId(request: TRequest) {
419 return md5FromObject({type: this.type, request});
420 }
421
422 createAPI(requestId: string): RequestRunnerAPI {
423 let api = {
424 invalidateOnFileCreate: glob =>
425 this.tracker.graph.invalidateOnFileCreate(requestId, glob),
426 invalidateOnFileDelete: filePath =>
427 this.tracker.graph.invalidateOnFileDelete(requestId, filePath),
428 invalidateOnFileUpdate: filePath =>
429 this.tracker.graph.invalidateOnFileUpdate(requestId, filePath),
430 invalidateOnStartup: () =>
431 this.tracker.graph.invalidateOnStartup(requestId),
432 replaceSubrequests: subrequestNodes =>
433 this.tracker.graph.replaceSubrequests(requestId, subrequestNodes),
434 };
435
436 return api;
437 }
438}