Resource Access API

Table of Contents

The backend exposes its model over the Resource Access, the connection the extension opens with Actifsource: Connect Resource Access. Everything the extension does - reading the model, changing it, searching, generating - goes through it. The interface is open: any client that speaks it can work on the same model and the same session state.

This chapter describes the protocol itself. The individual requests are listed in the specification.

Info

This is the interface the extension uses, not a stable public contract. It changes with the protocol version - see Protocol version.

Connecting

The backend listens on a WebSocket. Host and port are the ones configured for the extension, localhost and 8088 by default:

ws://<host>:<port>

Connect to the bare path. The path of the URL becomes a prefix of every method name, so a connection to ws://host:port/foo addresses the services foo/SelectJob, foo/Session and so on - and nothing answers them. The only user of a prefix is the MCP server under /mcp, which is a separate HTTP interface.

A text frame carries at most 5 MiB. The connection has no idle timeout. There is no handshake, no session id and no authentication: the first frame after the connection is open may already be a request.

The envelope

All traffic is JSON-RPC 2.0. A request names the service in method and carries an integer id:

{ "jsonrpc": "2.0", "id": 42, "method": "SelectJob", "params": { } }
{ "jsonrpc": "2.0", "id": 42, "result": { } }

The id must be a JSON number. A string id is read as absent, the frame is treated as a notification, and no answer is ever sent.

params always carries a type field that selects the request within the service. The services are:

method Purpose
SelectJob Reading the model - one request type per select job
UpdateJob Changing the model - one request type per update job
State Asking for what changed since the last call (long poll)
Session Save, undo, redo, revert, and forks of the session
Search Searching the model
Command Executing a model command, e.g. the code generator
Selection The shared selection between the clients of a session
Link Resolving a link to a resource
AutoCompletion Content proposals for the template editor
ConsoleLogger The log and the error reporter
Version The protocol version of the backend

An unknown method is answered - see Errors - with Request '<method>' is not handled: <the original frame>.

Reading the model

A select job is a request to SelectJob with type: "Selectable". The job itself sits in data, where its own type names the job:

{ "jsonrpc": "2.0", "id": 42, "method": "SelectJob",
  "params": {
    "type": "Selectable",
    "editContext": { "type": "GlobalEditContext" },
    "dependencyId": 7,
    "data": {
      "type": "SelectSimpleName",
      "node": { "type": "Resource", "guid": { "uuid": "4cf5ec36-..." } }
    }
  }
}
Key Required Meaning
type yes "Selectable"
data yes The job and its arguments, type names the job
editContext yes What the read sees - GlobalEditContext is the whole workspace
dependencyId no Marks the read, so that the client learns when its result goes stale
forkId no Reads the fork instead of the session

The answer wraps the job’s result the same way:

{ "jsonrpc": "2.0", "id": 42,
  "result": { "type": "Selectable", "data": { "type": "SelectSimpleName", "name": "Engine" } }
}

Streaming

A job that answers in several pieces puts a streamingId next to its data. The client asks for the next piece with a NextChunk request, and keeps asking as long as the answer carries a streamingId:

{ "jsonrpc": "2.0", "id": 43, "method": "SelectJob",
  "params": { "type": "NextChunk", "streamingId": 12, "canceled": false }
}

An answer without streamingId is the last one. That is the only end-of-stream signal. To stop early, send canceled: true on the next NextChunk.

sequenceDiagram participant C as Client participant S as Backend C->>S: SelectJob {type: Selectable, data: {...}} S-->>C: result {streamingId: 12, data: chunk 1} C->>S: SelectJob {type: NextChunk, streamingId: 12} S-->>C: result {streamingId: 12, data: chunk 2} C->>S: SelectJob {type: NextChunk, streamingId: 12} S-->>C: result {data: chunk 3} Note over C,S: no streamingId - the stream is finished

Staying up to date

A select result is a snapshot. A client that wants to know when the model has moved under it passes a dependencyId of its own choosing with the request. The backend remembers what the read touched, and reports once when any of it changes.

The report is not pushed. The client collects it from the State service, which holds the request open until something has changed:

{ "jsonrpc": "2.0", "id": 99, "method": "State",
  "params": { "type": "ResourceAccessObserver", "ids": ["SelectJob"], "force": false }
}
{ "jsonrpc": "2.0", "id": 99,
  "result": { "type": "ResourceAccessObserver",
              "states": [ { "type": "DependencyChanged", "serviceId": "SelectJob", "dependencyId": 7 } ] }
}
Key Required Meaning
ids yes The services to observe. Nothing is collected for a service never asked for
max no At most this many states per answer
force no true answers at once, with an empty list if nothing changed

A dependency fires once and is then forgotten. A client that wants to keep watching re-sends the select job with a new dependencyId.

sequenceDiagram participant C as Client participant S as Backend C->>S: SelectJob {dependencyId: 7, ...} S-->>C: result {data} C->>S: State {ids: [SelectJob]} Note over C,S: held open Note over S: the model changes S-->>C: result {states: [{dependencyId: 7}]} C->>S: SelectJob {dependencyId: 8, ...}

Changing the model

An update job is a request to UpdateJob with type: "Modifiable":

{ "jsonrpc": "2.0", "id": 43, "method": "UpdateJob",
  "params": {
    "type": "Modifiable",
    "editContext": { "type": "GlobalEditContext" },
    "autoSave": true,
    "data": { "type": "UpdateName", "resource": { }, "name": "Engine" }
  }
}

It differs from a select job in three points: autoSave is required, there is no dependencyId (an update does not observe anything), and the answer is not wrapped. The job’s result is the result itself:

{ "jsonrpc": "2.0", "id": 43, "result": { "type": "UpdateName" } }

autoSave: false leaves the change in the session, to be written by Session / Save - or dropped by Revert.

Errors

A failed request is answered with a successful JSON-RPC response. The WebSocket transport never sends a JSON-RPC error member; the failure sits in result, under error:

{ "jsonrpc": "2.0", "id": 42,
  "result": { "error": { "name": "ResourceNotExistException",
                         "message": "Resource Engine does not exist!",
                         "stack": "..." } }
}

A client decides that a request failed by the presence of result.error, not by the shape of the frame. stack is optional. name is one of:

name Meaning
Error Anything the backend does not classify further
ResourceAccessException The request could not be handled, e.g. an unknown method
ConnectionException The backend is shutting down
ResourceNotExistException The addressed resource does not exist (any more)
RootResourceNotExistException The addressed root resource does not exist (any more)
NoOperationException Undo or redo with nothing to undo or redo

The numeric JSON-RPC error codes (-32700, -32602, -32603) occur only on the MCP interface.

Protocol version

The backend answers its protocol version:

{ "jsonrpc": "2.0", "id": 1, "method": "Version", "params": { "type": "ProtocolVersion" } }
{ "jsonrpc": "2.0", "id": 1, "result": { "type": "Version", "version": "1.0.0.+390187895" } }

The extension asks this first and disconnects on any difference - the strings must be equal, the version is not a range. A client of its own is not forced to check, but a version it was not written against gives no guarantee about any of the requests below.

Specification

The full request and response of every job and service is specified in AsyncAPI and listed on the pages below. The specification is generated from the backend, so it describes the version of the backend it ships with.