1 |
|
2 |
|
3 | import type {AbortSignal} from 'abortcontroller-polyfill/dist/cjs-ponyfill';
|
4 | import type {File, FilePath, Glob} from '@parcel/types';
|
5 | import type {Event} from '@parcel/watcher';
|
6 | import type {NodeId} from './types';
|
7 |
|
8 | import invariant from 'assert';
|
9 | import nullthrows from 'nullthrows';
|
10 | import {isGlobMatch, md5FromObject} from '@parcel/utils';
|
11 | import Graph, {type GraphOpts} from './Graph';
|
12 | import {assertSignalNotAborted} from './utils';
|
13 |
|
14 | type SerializedRequestGraph = {|
|
15 | ...GraphOpts<RequestGraphNode, RequestGraphEdgeType>,
|
16 | invalidNodeIds: Set<NodeId>,
|
17 | incompleteNodeIds: Set<NodeId>,
|
18 | globNodeIds: Set<NodeId>,
|
19 | unpredicatableNodeIds: Set<NodeId>,
|
20 | |};
|
21 |
|
22 | type FileNode = {|id: string, +type: 'file', value: File|};
|
23 | type GlobNode = {|id: string, +type: 'glob', value: Glob|};
|
24 | export type Request = {|
|
25 | id: string,
|
26 | +type: string,
|
27 | request: mixed,
|
28 | result?: mixed,
|
29 | |};
|
30 |
|
31 | type RequestNode = {|
|
32 | id: string,
|
33 | +type: 'request',
|
34 | value: Request,
|
35 | |};
|
36 | type RequestGraphNode = RequestNode | FileNode | GlobNode;
|
37 |
|
38 | type RequestGraphEdgeType =
|
39 | | 'subrequest'
|
40 | | 'invalidated_by_update'
|
41 | | 'invalidated_by_delete'
|
42 | | 'invalidated_by_create';
|
43 |
|
44 | const nodeFromFilePath = (filePath: string) => ({
|
45 | id: filePath,
|
46 | type: 'file',
|
47 | value: {filePath},
|
48 | });
|
49 |
|
50 | const nodeFromGlob = (glob: Glob) => ({
|
51 | id: glob,
|
52 | type: 'glob',
|
53 | value: glob,
|
54 | });
|
55 |
|
56 | const nodeFromRequest = (request: Request) => ({
|
57 | id: request.id,
|
58 | type: 'request',
|
59 | value: request,
|
60 | });
|
61 |
|
62 | export 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 |
|
70 |
|
71 | unpredicatableNodeIds: Set<NodeId> = new Set();
|
72 |
|
73 |
|
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 |
|
81 | return deserialized;
|
82 | }
|
83 |
|
84 |
|
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 |
|
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 |
|
233 |
|
234 |
|
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 |
|
274 | export 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 |
|
347 | type RequestRunnerOpts = {
|
348 | tracker: RequestTracker,
|
349 | ...
|
350 | };
|
351 |
|
352 | export type RunRequestOpts = {|
|
353 | signal?: ?AbortSignal,
|
354 | parentId?: string,
|
355 | |};
|
356 |
|
357 | export type RequestRunnerAPI = {|
|
358 | invalidateOnFileCreate: Glob => void,
|
359 | invalidateOnFileDelete: FilePath => void,
|
360 | invalidateOnFileUpdate: FilePath => void,
|
361 | invalidateOnStartup: () => void,
|
362 | replaceSubrequests: (Array<RequestGraphNode>) => void,
|
363 | |};
|
364 |
|
365 | export function generateRequestId(type: string, request: mixed) {
|
366 | return md5FromObject({type, request});
|
367 | }
|
368 |
|
369 | export 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 | ?
|
389 | (this.tracker.getRequestResult(id): any)
|
390 | : await this.run(requestDesc, api);
|
391 | assertSignalNotAborted(signal);
|
392 |
|
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 |
|
403 |
|
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 |
|
411 |
|
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 | }
|