Tightener Architecture - zwettemaan/TightenerDocs GitHub Wiki

The Tightener API

Tightener provides the low-level ‘glue’ to communicate between nodes. These nodes can run on Mac, Windows or Linux.

The Tightener API is like Adobe Bridge (the protocol, not the application).

For the Tightener project I've borrowed ideas from Adobe Bridge, AppleEvents, Node/JS, Python...

Tightener is written in plain C++ 11, and has no external dependencies. It can be compiled into standalone apps and into plug-ins.

Tightener Bus Complete

https://github.com/zwettemaan/TightenerDocs/blob/main/TightenerConcept.pdf

Nodes

Each application, process, or plugin which embeds the Tightener API is called a node.

One app could host multiple Tightener nodes - e.g. InDesign could have multiple Tightener-enabled plug-ins and each one would be a separate Tightener node.

Scripts are written in whatever language we desire, and use ‘their’ Tightener node to communicate with the app or plug-in nodes.

The Tightener API allows message passing between nodes by way of unidirectional IPC/named pipes.

For two Tightener nodes to interact directly, they need to be on the same computer.

Remote nodes cannot interact directly because Tightener has no networking functionality.

For remote nodes to interact, we need to set up two or more gateway nodes. These nodes handle the connection between the two computers.

Tightener can use these gateway nodes, but it does not know how these nodes work their magic and get the data across.

Tightener can be integrated into applications, plug-ins, Node/JS scripts, Python scripts... and once it is integrated it allows nodes to interact via message passing.

One Named Pipe Per Node

On a single workstation, we would have a number of active Tightener nodes, each encompassing a number of services.

We might have an InDesign node, an InDesign 'plug-in' node, an Illustrator node, a Coordinator node...

On each computer, there is a single 'central' Tightener node, the Coordinator. This is a C++ app process which runs in the background.

The Coordinator is the switchboard which ties all other nodes together.

Each active node, including the Coordinator, 'owns' a single named pipe.

An active node will create or re-create its 'own' named pipe in a pre-determined location, and it will read and monitor this pipe as long as the node is active. When an active node notices its own pipe has disappeared, it will re-create it.

The only node allowed to delete named pipes is the Coordinator.

Other nodes will create pipes, and the Coordinator will delete them.

The Coordinator does this when it determines the node associated with the pipe has died.

Other nodes on the workstation communicate via the Coordinator by writing to the Coordinator's pipe.

The Coordinator communicates back to the other nodes by writing to their associated pipes.

A node will advertise its presence to the Coordinator by (re)creating an associated named pipe in a predetermined location.

The Coordinator monitors the appearance of these pipes and will 'ping' any new node it discovers.

The Coordinator also tracks which nodes are alive or dead.

If a node does not respond in a timely manner, the Coordinator will delete the associated named pipe for the dead node.

If and when the missing node is restarted later on, this pipe will be re-created by the newly started node.

Services

Each Tightener node embeds one or more 'services'.

Tightener does not use multi-threading. Tightener services are cooperative multitaskers.

Interaction between Tightener nodes is implemented by message passing between services.

Two services can interact via Tightener - whether they are embedded in the same node, embedded in two different nodes on the same machine, or embedded in two nodes on two remote machines.

Some services are very simple: there is a 'null' service which serves as a bit bucket, a 'ping' service which reflects anything sent to it back to the sender.

DOM Services

DOM Services provide an entity–attribute–value model which can be interacted with by sending messages with queries to the service.

Tightener uses JSON for transmitting structured data between services

The data provided by these DOM Services readily maps to JSON format.

For example, the InDesign DOM presents itself as a DOM Service embedded in the InDesign node.

DOM Selectors

The Tightener API uses a construct called a selector to query data inside a DOM.

DOM Selectors are somewhat similar to XPath for XML.

Selectors are meant to offer similar functionality to what is offered by collections in InDesign ExtendScript: they allow getting or setting many properties on many objects with a single command or query.

A selector is composed of one or more segments, delimited by slashes.

Slashes at the start and finish are optional, multiple consecutive slashes are equivalent to just a slash (e.g. /name//// is equivalent to name).

Each segment addresses a level within the entity–attribute–value model.

  1. A segment can be a positive integer, for addressing array-like entities.

  2. A segment can be a string, for addressing associative-array-like entities. If the attribute name itself contains slashes, you need to escape it with a backslash.

E.g. if the attribute name is a/b you might encode the segment as /a\/b/. Note that in a JavaScript string literal you would need to escape the escape - e.g. var selector = "/a\\/b/";

  1. A segment can contain a quantity selector, followed by a RegExp, for matching zero or more nested entities.

Regular expression segments are prefixed with a quantity selector, which starts with one of the characters !, *, ?, + or {.

Quantity selectors are NOT part of the RegExp.

Apart from the !, they do look similar to how RegExp work, this to help memorizing which one to pick:

! -> exactly 1 time
? -> 0 or 1 time
* -> 0 or more times
+ -> 1 or more times
{n,} -> n or more times
{,n} -> 0 to n times
{n,m} -> between n and m times

Mock example ('...' means: '...and so on').

{
  documents: [
     {
        name: "Untitled-1",
        pageItems: [
          {
             name: "Text Frame 1",
             ...
          },
          ...
        ],
        ...
     },
     {
        name: "Untitled-2",
        pageItems: [
          ...
        ],
        ...
     },
     ...
  ],
  "name": "Adobe InDesign 2021",
  "fullPath": "/Applications/Adobe InDesign 2021/Adobe InDesign 2021.app"
}

Sample selectors and their values:

    name              –>   "Adobe InDesign 2021"
    /name/            –>   "Adobe InDesign 2021"
    documents/0/name  ->   "Untitled-1"
    !d.*/![0-9]+/name ->   [ "Untitled-1", "Untitled-2" ]
    !d.*/+.+/name     ->   [ "Untitled-1", "Untitled-2", "Text Frame 1"... ]

Unpacking this:

!d.*: ! = exactly one time; d.* = a letter 'd' followed by zero or more characters. This matches the 'documents' attribute name.

+.+: + = 1 or more times. This will match multiple nested levels inside the structure. .+ = one or more characters. This example matches documents/0/name, documents/0/pageItems/0/name...

FileSystem DOM Service

Mainly to help me test, I've added a FileSystem DOM service to Tightener.

This offers an entity–attribute–value model on top of the computer's file system.

A FileSystem DOM node looks like:

{
  "name": "<file name>",
  "path": "<file path>",
  "isDir": <true/false>,
  "isFile": <true/false>,
  "isPipe": <true/false>,
  "fileSize": <file size>,
  "fileList": [
     "<file name>",
     "<file name>",
     ...
  ],
  "contents": "file contents"
}

The FileSystem DOM Service is part of a node I use for testing bidirectional communication between the Coordinator and an external node.

⚠️ **GitHub.com Fallback** ⚠️