Skip to content
This repository has been archived by the owner on Dec 13, 2018. It is now read-only.

Remote Nuclide Services

Peter Hallam edited this page May 16, 2016 · 2 revisions

Create a service that transparently runs on both local and remote files.

When to Use Nuclide RPC

Nuclide RPC should be used to encapsulate operations that will need to run on both local and remote filesystems. Once you define a service, clients that request to use the service will either be directed to a local implementation, or to an RPC proxy.

Here are some example uses for Nuclide RPC service:

  • FindInProjectService handles the functionality for performing search on a remote directory (the local operation is handled already by Atom). FindInProjectService
  • InfoService can be used to query information about a remote host, such as the current server version. InfoService
  • BuckProject can be used to query information and run commands on Buck projects. BuckProject

Creating a Service

Creating and using a service consists of three steps:

  1. Create the local implementation of the service.
  2. Register the service.
  3. Consume the service.

Once these steps are complete, you can request an instance of the service to use, through the nuclide-client package. The steps are described in detail below.

Step 1: Define & implement the Service API

Service APIs are defined in a ES6 file with Flow type annotations. Say we want to define a CounterService, which will let us have remote 'Counter' objects. We create a file CounterService.js as follows:

name=CounterService.js

  'use babel';
  /* @flow */

  // Global list of counters.
  var counters: Array<Counter> = [];

  export class Counter {
    _count: number;

    // Create a new counter with the given initial count.
    constructor(initialCount: number) {
      // Set initial count.
      this._count = initialCount;
      
      // Add this counter to global list.
      counters.push(this);
    }
    // Get the current value of a counter.
    async getCount(): Promise<number> {
      return this._count;
    }
    // Add the specified value to the counter's count.
    async addCount(x: number): Promise<void> {
      this._count += x;
    }

    // Dispose function that removes this counter from the global list.
    async dispose(): Promise<void> {
      // Remove this counter from the global list.
      counters.splice(counters.indexOf(this), 1);
    }

    /** Static Methods */

    // List all of the counters that have been created.
    static async listCounters(): Promise<Array<Counter>> {
      return counters;
    }

    // A static method that takes a Counter object as an argument.
    static async indexOf(counter: Counter): Promise<number> {
      return counters.indexOf(counter);
    }
  }

Take note of the following points:

  • All exported type aliases, functions, and class declarations in a service file are considered part of the service's remote API.
    • Class declarations can consist of static methods and instance methods.
    • Non-exported functions, classes and type aliases are not considered part of the remote API.
    • Variables are never considered part of the remote API regardless of whether they are exported or not.
    • Imported types (other than NuclideUri) may not be used in remote APIs. If you want to use a type in a remote API then you must move that type to the service file.
    • If you want to export types/functions/classes but not make them part of the remote API then you should move them to a different file.
  • Class members, functions and types whose name begins with an '_' are private and are not considered part of the remote API.
  • All arguments and return values of remote APIs must be fully typed.
    • See the Valid Types section for a list of all currently supported types.
  • You can use type aliases, defined with the format export type AliasName = AliasedType;
  • The return value of a method must be one of the following:
    • void - For 'fire-and-forget' functions.
    • Promise<T> - For functions that eventually return a value of type T.
    • Promise<void> - For functions that don't return a value, but can still be "waited" on.
    • Observable<T> - For functions that return a stream of data. See nuclide/using-observables for more details.
  • If an argument or a return type is a file path, you should use NuclideUri instead of string. This will allow you to send URI or paths from the client, but have the server always receive an absolute path.
  • You must implement the dispose method, so that objects that are created can be properly cleaned up when a client session is lost.
  • This implementation could be running either on the server (inside a Node runtime), or locally (inside the Atom runtime).

Validation Script

If there's a doubt as to whether or not a service definition is formatted correctly, there is a validation script available under nuclide-service-parser/bin/validate. This script will attempt to parse the definition, reporting errors if there are any.

Step 2: Register the Service

Register your service in nuclide-server/services-3.json:

name=services-3.json

  [
    ...
    {
      "implementation": "path/to/CounterService.js",
      "name": "CounterService"
    }
  ]

For implementation path, currently we support two kinds of paths:

  • Recommended: Create the implementation class in a separate package, add that package to the dependencies of nuclide-server, then use ${package_name}/relative/path/to/class.js
  • Not Recommended: Put the implementation in the nuclide-server package, and provide a path relative to services-3.json.

Step 3: Consume the Service

Now, we can consume the service in Nuclide code. First, your package needs to depend on nuclide-client.

name=consumer.js

  import {getServiceByNuclideUri, getService} from 'nuclide-client';

  import typeof * as CounterService from 'CounterService';  

  // If you have a NuclideUri, which could either be a local path like 
  // `/path/file` or a remote one like `nuclide://host:port/remote/path`,
  // use getServiceByNuclideUri to get a service.
  var service = getServiceByNuclideUri('CounterService', $localOrRemoteUri);

  // Or you have hostname, you could use getService, and use null as hostname
  // if you want to get a local service.
  var service: CounterService = getService('CounterService', $hostname)

  // Create an instance of the Counter class and perform some operations.
  var counter = new service.Counter(3);
  await counter.addCount(1);
  var count = await counter.getCount();

  // Call some static methods.
  var counters = await service.Counter.listCounters();

  // Dispose the counter object.
  await counter.dispose();

Valid Types

The following types can be sent as an argument, or returned as a value from any function or method:

  • Primitives
    • string
    • number
    • boolean
  • These types can handle any JSON serializable value:
    • mixed
    • any
    • Object - Must be an object not a primitive value.
  • NuclideUri, used for representing file paths
  • Nullable Types, written as ?T
  • Collections
    • Array<T>
    • Set<T>
    • Map<K, V>
    • Object Types, written in the form { field: T; ... }
      • Keys can be optional, which is distinct from a required key that points to a nullable value.
    • Tuple types, written as [T1, T2, ...]
  • Date
  • Buffer
  • RegExp
  • fs.Stat
  • Type Aliases defined in the current service file.
    • Must be defined using the syntax export type AliasName = ...;
  • Any exported class defined in the current service file.

Testing

When your new service is used locally, calls to the service are routed to the service implementation directly. There is an option 'Use RPC for Local Services' which will force all local calls to all services to be routed through the RPC marshalling layer. This enables testing a remotable service definition locally which is easier than sync-ing your changes to a remote machine.