20221001 Orbital 2022 M2 Services Principles - orbitalfoundation/wiki GitHub Wiki

Service Prototype

Services in Orbital are designed to help compose rich applications out of small pieces loosely joined.

A Service in Orbital is a class that may have specific public interfaces (they don't need to have these interfaces but they cannot participate in the larger body of interactions without them). A typical service looks like this:

class Service {
	constructor(args) -> do any initial bare bones construction
	route(filter,channel) -> attach a listener to this service and forward route anything we think is interesting to them
	resolve(datagram) -> handle incoming messages
	my_naked_method(arg1,arg2) -> handle an incoming message but take advantage of traditional compiler method signatures
}

Wiring Services Together

Although Services look like ordinary classes (and in fact they may be) the methods on a service should be treated as 'channels' that are asynchronous message handlers rather than always immediately returning results. Services in Orbital may be sent messages via a resolve(datagram) method or by any developer defined method call such as my_naked_method(arg1,arg2). Many (but not all) Services implement route() and will publish messages to registered observers. Services publish whatever they want, but the design intent is that Services echo relevant state to other parties that may care.

Here is example pseudo-code that creates a database that stores state, and then also attaches a view to it as an observer. Writes to the database are echoed to the view. A service can publish anything it wants at any time to any registered observer - but in this case a practical implementation is to merge the previous state of a written object with new state and then echo the merged result to observers:

let db = new DB()
let view = new View()
db.route("/mypublicdata/*",view)
db.save({uuid:"/mypublicdata/mysphere",kind:"sphere",radius:1000,color:"blue"})

Underlying Principles

  1. The Service Concept. Services are the core unit of execution for Orbital. Any developer working within Orbital is expected work with services. All executable concepts such as 'app', 'threads', 'applications', 'agents' or even 'event handlers' are implemented as services in Orbital. This stems from the agent-model design goals of Orbital and an emphasis on late binding distributed code.

  2. Services are Proxies. A service is conceptually a 'proxy' for a capability. It (conceptually) is reached by passing messages, and it returns messages. The computation or execution of the capability can be on remote hardware. Although it is implemented as a class, and can have methods, it must be thought of as a 'channel' or message passing system.

  3. Service Source Code Files. Writing a new service means complying with a few rules. For now a service must be defined as a single javascript class. That class is defined in a single file and must be exported from the file (using export).

  4. Service Loader. A dynamic loader (see the pool manager discussed later) imports that file and produces the class and imposes security. The file export may be a singleton instance, in which case you should export a singleton instance. The class may be multiply instanced, in which case you should export a class reference. Today there is only one file per class and one class per file - later multiple classes per file may be supported.

  5. Service Singleton. Often a service handle is a proxy on a singleton (for example there is a single physical view in a vr helmet), but callers may instance a service which proxies interactions to the shared view. Each view instance can have its own state.

  6. Services are in Javascript. Today we support only Javascript. Later WASM/RUST will be supported and should be a largely invisible change. Regardless of language there are some special rules around sync/async methods which we will go into later. In general at run time a service is loaded, compiled and then a class instance (either a singleton or a dynamic instance) is typically (although not necessarily) registered with a pool manager.

  7. Service Source Code Files physical location. The physical location of the file is fairly open. It must be in a location accessible to an instance of Orbital. Currently that means the file must be physically located on a file system accessible to Orbital or on the Internet at large reachable via http. Later on we may support streaming of services as streams through other mechanisms.

  8. Service Class Constructor Method. The service constructor is typically passed a back pointer to the pool manager. This is useful for discovery of yet other services.

  9. Service resolve() Method. Services by convention may support a resolve() method that can receive and handle a message form of a method invocation. But if not the pool can destructure a hash and invoke methods on a service, otherwise you can always call the service method yourself if you have a handle on the service and it exposes the method. There are reasons to call either a naked method or resolve. Resolve() can handle an array of many method invocations at once. On the other hand a naked method has method signatures.

  10. Service Method Signatures. A design principle is that users interact with service methods that accept arguments. We want to take advantage of method parameter signatures and other conventions of programming, and not force users to use messages. That means typically users interact with what appear to be first class objects such as "a database" or "a computer vision library".

  11. Services as Scripting Glue. Most of what the source code within a given service is doing is largely scripting glue that is calling methods on other services. Most applications are effectively 'glue' between third party services. Often you will obtain a handle on some other service and then call methods on that service to do some work.

  12. Service methods with await syntax. A service method can use an await/promise. This can also return results if desired. Note that there's a bit of a hidden design tension here. The service method is effectively is acting like a proxy for some deeper asynchronous process. As a design rule it's best to try avoid synchronous methods.

  13. Service methods without await. A service method can in fact immediately return results. In this case however the service is acting as a proxy for some other asynchronous handler - there is simply a class of larger slower compute problems than you cannot expect to do synchronously in this way.

  14. Service Events. A recommended pattern for getting events back from a service is to register a callback handler with that service. For example "myview.listen_to_collision_events( callback )". We have a convention about how the returned events should be shaped (basically we have a single global structure for an event). It's important to understand that unlike ordinary library calls that you may be used to - the default pattern of servies is that they are built on an agent model. Ordinary method calls on a service such as "myview.create_box()" are by default actually implemented as messages. Messages to a service are guaranteed to be in locally invariant order but are not guaranteed to be synchronous nor globally invariant (multiple parties can write to a service and the order is not guaranteed between those parties). Methods do not by default return results.

Pool Manager Service

The pool manager manages other services - it is conceptually the micro kernel or thread manager of all other services in an instance of Orbital.

To bootstrap Orbital the first thing is to load a copy of the pool manager service and use it to spawn other services. If you are standing up your own copy of Orbital you should do this first. Otherwise if you just want to run a service within some existing harness then you don't need to start the pool manager. Note that of course you don't have to use a pool manager but if you don't then we cannot provide any security or service loading for you - and you're basically just writing your own app with your own policies - not an Orbital app.

Services can be registered with a 'canonical' path such that they can always be rediscovered at that path, or they can be registered dynamically. In some cases a developer may want multiple instances of a given service, and as well sometimes a service instance is actually just a proxy or a facade for a singleton, or even for a remote network connection.

Application Level Services

  1. Applications. A full blown user application is also a service. It starts up and runs just like a service. Conceptually an application can be thought of as a manifest (similar to say index.html except that it is executable code) in that this is your opportunity to ask the pool manager to manufacture all the other services you would like to use, and to also load up any art assets. Services are started from a specific 'place' and by default any related assets can be located by a relative path in that place. It is convenient therefore to stuff all the files related to a single application in a single folder (but not necessary).

  2. Application Paths To Relative Resources. An absolute path location for other naked resources is also passed to your service. Most all other services you want to load or talk to, any low level libraries you want to directly embed, any resources and so on can be fetched via the pool manager, the resource path, or using an URL. A service itself can be thought of as a manifest - specifying other services and resources to load (see Applications).

Typical Services Bootstrapping for a Server

Orbital doesn't necessarily have to be used in a hub-and-spoke client-server model but it often will be. Orbital is meant to be used as a sense making tool and in effect it makes sense to run as a 'personal server' - a persistent durable background tool where views consult the persistent state. Therefore a client/server pattern will be quite common. In current demos a server is bootstrapped like so:

// load pool manager by hand
import Pool from '#orbital/sys/services/pool.js'
let pool = new Pool()

// start db
let db = await pool.load({urn:"*:/sys/services/db"})

// start http
let http = await pool.load({urn:"*:/sys/services/http",args:{resources:import.meta.url}})

// start long sockets
let net = await pool.load({urn:"*:/sys/services/net",args:{server:http}})

// add some routes
net.route({filter:"*:/sys/services/db",channel:db})
db.route({filter:"*",channel:net})

// for now explicitly let http run forever - todo look for asynchronous options later - currently server is driven by inbound traffic
http.runforever()

Typical Bootstrapping for a Client

The current demos for Orbital run a default client side app in a web page. See sys/apps/server/public/client.js for details.

The bootstrapping here (in client.js) has just a few chores:

  • it runs in a browser
  • it starts a client side copy of orbital
  • it runs an 'appropriate' app based on some strategy (for now hardcoded)

Typically this is what it looks like:

// load pool service by hand
import Pool from './sys/services/pool.js'
let pool = new Pool()

// run the app
pool.load({urn: "*:/sys/apps/multiverse/multiverse",canonical:true})