Tightener Architecture - zwettemaan/TightenerDocs GitHub Wiki

Tightener Architecture

Tightener provides the low-level glue for communication, orchestration, and object model access between nodes. A node can be a native application, plug-in, command-line tool, gateway, scripting bridge, or hosted runtime.

The architecture borrows ideas from Adobe BridgeTalk, AppleEvents, Node/JS, Python, and DOM selector/query systems, but the goal is smaller and more controlled: reliable creative-app orchestration without depending on a fragile external scripting runtime.

Tightener is written in plain C++ 11. The basic core is intentionally dependency-light and can be compiled into standalone apps and 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 can be written in TQL, ExtendScript, UXP JavaScript, Python-facing tooling, Xojo, command-line wrappers, or another host language. They use their local Tightener node to communicate with app or plug-in nodes.

The Tightener API allows message passing between nodes. Local communication uses OS-level IPC/named-pipe style mechanisms. Remote communication is handled by gateway nodes and related infrastructure.

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

Remote interaction is routed through Tightener-enabled gateway nodes. The core runtime should not force one networking strategy on every host; gateways provide the network bridge appropriate for the deployment.

For remote nodes to interact, two or more gateway nodes handle the connection between computers.

Tightener can use these gateway nodes without every embedded host needing to know the details of transport, deployment, or discovery.

Tightener can be integrated into applications, plug-ins, Node/JS scripts, Python-facing tools, Xojo tools, and command-line processes. Once integrated, nodes interact via message passing and TQL/data requests.

TQL In The Architecture

TQL is the built-in language for asking a node to do useful work near the object model it owns. Instead of making hundreds of small remote property requests, a caller can send a compact TQL expression/script to the target node and receive a structured result.

This is especially important for InDesign automation. An InDesign node can expose the InDesign object model to TQL, evaluate a query locally, and return data or error values to the caller.

See TQL and the TQL Master Document.

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 services are cooperative multitaskers. Long operations are split into tasks and yield at known points. This avoids forcing script authors to deal with locks or thread synchronization and keeps host applications responsive.

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** ⚠️