The Qworum platform, version 1.0

Summary

Qworum is a variant of the World Wide Web that is especially suitable for applications.

This specification defines the new Web browser capabilities that the Qworum platform is providing to Web-based applications.

1. Introduction

The World Wide Web ♦︎ was initially conceived as a decentralised information system consisting of interlinked documents, yet its potential as a platform for applications soon became apparent. The ongoing effort that has been undertaken by the ICT community since the Web's early days in order to better support Web applications has sofar produced various client-side and server-side upgrades to the initial Web, such as JavaScript and application servers.

Yet there is room for improvement, particularly regarding the decentralisation of the front-ends of Web applications. In today's Web, an application is contained within a single Web origin (example: https://example.com) ♦︎, and frequently even within a single Web page. This limits the amount of functionality that can be shared amongst application developer teams, and results in considerable duplication of effort. (One notable exception to this picture is OAuth-based services which can be called remotely by applications.)

Qworum enables new gains in productivity for the software community by making it possible to build Web applications that are composed of interactive services ("Qworum services") that are hosted anywhere on the Web and that are callable remotely.

It is worth noting that Qworum services are of practical use even if the caller of a service has the same Web origin as the called service. This is because Qworum services can be inserted in any user flow without disrupting it, similarly to functions in conventional programming. For example, with Qworum an application no longer needs to pass a return path to the login dialog when it requires the end-user to sign in before accessing a restricted page. Again, this is similar to conventional programming where a function does not know how the execution proceeds when its own execution finishes.

As a foretaste, here is how an e-commerce site might call a remote shopping cart service:


      // Step 1: Import the Qworum library
      { QworumScript as qs, Qworum } fromhttps://esm.sh/gh/doga/qworum-for-web-pages@1.7.1/mod.mjs";

      const
      // Data values
      Json         = qs.Json.build,
      SemanticData = qs.SemanticData.build,

      // Instructions
      Data     = qs.Data.build,
      Return   = qs.Return.build,
      Sequence = qs.Sequence.build,
      Goto     = qs.Goto.build,
      Call     = qs.Call.build,
      Fault    = qs.Fault.build,
      Try      = qs.Try.build,

      // Script
      Script = qs.Script.build;

      // Step 2: create and run a Qworum script
      await Qworum.eval(
        Script(
          Sequence(
            Call(["@", "shopping cart"], "https://shopping-cart.example/view/"),
            Goto('/home/')
          )
        )
      );
      
Figure 1.1: Example of a Qworum script that is run by a Web page in a Web browser.

2. Terminology

2.1. Phase

An HTTP(S) ♦︎ request-response pair.

Phase
Figure 2.1.1: Phase.

2.2. Single-Phase Service Call

A call to a Web API that consists of a single phase. Calls to Web APIs that conform to conventional specifications such as REST ♦︎ or XML-RPC ♦︎ are always single-phase calls.

Single-Phase Service Call
Figure 2.2.1: Single-Phase Service Call.

2.3. Multi-Phase Service Call

A call to a Web API that consists of one or more phases. Calls to Web APIs that conform to the Qworum specification are multi-phase calls.

During a multi-phase call, the first request that is sent by the user agent to the server contains 0, 1 or more call arguments. The service then returns a response that:

  • contains a call result (in which case the call is a single-phase call), or
  • allows the user agent to initiate a second phase for the current call.

In this manner, each phase of a multi-phase call can choose to return a result or continue the current call with another phase. Note that Qworum does not define a content format for ending a call. Rather, each phase normally sends an HTML file to the client (except in case of in-page navigation), and the JavaScript that runs in the client determines whether the phase ends the current call or not.

Figure 2.3.1: Example of a Multi-Phase Service Call.

Qworum mandates that each phase of a particular multi-phase call have the same Web origin, otherwise an * origin fault will be raised during run-time.

2.4. The Qworum Script

Qworum scripts contain instructions that are to be executed by Web browsers. The root element of Qworum scripts must be an instruction. Qworum scripts can also contain data values.


// Add an article to a shopping cart.
const
article  = {
  "id"   : "8b1d5802",
  "title": "Classic ankle boots",
  "price": {"EUR": 29.99}
},
script = Script(
  Sequence(
    Call(
      ['@', 'shopping cart'], 'https://shopping-cart.example/add-article/',
      {name : 'article', value: Json({article})}
    ),
    Goto('index.html')
  )
),
button = document.getElementById('add-to-cart-button');

button.addEventListener('click', async () => {
  await Qworum.eval(script);
});
        
Figure 2.4.1: Using JavaScript for generating and running a Qworum script in a Web page.

2.5. Service Composition

The mechanism by which multi-phase calls perform other nested multi-phase calls during their execution. Nested calls are performed between two consecutive phases of the call that initiated the nested call.

When a call must perform a nested call, one of its phases runs a script that contains a call instruction.

Figure 2.5.1: Example of a nested service call.

2.6. Interactive Service

Each phase of a multi-phase service returns an HTML page ♦︎ to the browser. Services can, but aren't obliged to, use these pages to interact directly with the end-user.

Figure 2.6.1: An interactive service call.

For example, a shopping cart service may provide a method that shows shopping cart contents to the end-user by offering this type of interactivity.

2.7. Execution State

In Qworum, each browser tab has its own execution state, which consists of a call stack that contains one or more call frames.

The topmost call frame pertains to the call currently being executed, and the remaining call frames contain the states of calls whose executions are currently suspended.

Each call frame belongs to a Qworum object, and contains the state of the call.

Call frames and Qworum objects have an internal state that is stored in zero or more data containers and/or Qworum objects.

Figure 2.7.1: The execution state of a browser tab.

2.8. Qworum Object

In conventional object-oriented programming, objects are used for storing state that is shared between multiple calls to one or more functions, which are the object's methods. Qworum is porting this concept into the Web environment in the form of Qworum objects.

A Qworum object, then, is a container for run-time state that is shared between different calls. Qworum objects are used for storing datas (which point to data) and other Qworum objects.

Qworum objects are needed for storing the states of services such as shopping carts that must remember their contents across different calls incoming from an e-commerce site for example. In the absence of Qworum objects, Qworum would only be able to support services where each incoming call is independent from one another, which is the case for user authentication services and payment processing services among others.

Here is how Qworum objects are used in practice:

  • Each multi-phase call has a Qworum object that is the owner of the call. When initiating a call, the call's owner object is created by the user agent if it does not already exist in the browser tab's execution state.
  • Each Qworum object has a Web origin which is determined when the object is first used as the owner of a call. All phases of all calls which are owned by a Qworum object must have the object's origin, otherwise an * origin fault will be raised during run-time.
  • Qworum objects can be stored in call frames, or as the main object of an execution state.
Figure 2.8.1: The Qworum object.

2.9. Qworum Class

Qworum does not mandate that all Qworum objects stored in an execution state have different origins. It follows that calls to the same URL can be made using different owner objects, even within the same browser tab. This is how the concept of Qworum classes emerges; not as a browser feature, but rather as an aid for designing Qworum-based applications and services.

Formally, we can define Qworum objects to be instances of Qworum classes, and also that each Web origin can host any number of Qworum classes.

Figure 2.9.1: Qworum classes and objects.

Going back to our shopping cart example, we can imagine an e-commerce site that allows its users to create several shopping carts on the same shopping cart service, for example one "Home" cart, one "Work" cart and one "New year's eve party" cart.

3. Instructions

This section lists the instructions that are available to Qworum scripts. When evaluated by the user agent, all instructions will yield a value (which is Data), except for the Goto and Fault instructions.

3.1. Data

The Data instruction allows writing to, and reading from, a data container that is stored in an execution state. The current call has read/write access to data in the following locations:

  • the current call frame,
  • the current call's owner object,
  • nested objects contained in the current call's owner object.

The location of the data container that is being accessed is determined by its precise path, which is an array of strings:

Within this array, some strings are treated differently:

  • any string that starts with @ denotes the current call's owner object, and
  • strings starting with $, # or 🤖 are reserved and should not be used at this time.

3.1.1. Reading from a data container

Data stored in Qworum sessions can be read directly by JavaScript scripts that run in Web pages:


const 
// Read the line items (a ) from the shopping cart Qworum object that is
// a property of the current Qworum object.
lineItemsData = await Qworum.getData(['@', 'shopping cart', 'line items']), // qs.Json instance
lineItems     = lineItemsData?.value, // parsed JSON (one of JavaScript object, array, null, undefined, string, number, boolean)

// Read data that is stored locally in the current call.
localData  = await Qworum.getData('local data'),
localData2 = await Qworum.getData(['local data']); // same data as localData
        
Figure 3.1.1.1: Reading session data directly from a JavaScript script.

The data can also be read from within Qworum scripts:


await Qworum.eval(
  Script(
    Sequence(
      // Read the line items data, and copy it to a container that is local
      // to the current call.
      Data('copy of line items', Data(['@', 'shopping cart', 'line items'])),

      // Continue the current call.
      Goto('phase-2.html')
    )
  )
);
        
Figure 3.1.1.2: Reading and writing session data from a Qworum script.

If the data container or one of the Qworum objects in the container's path does not exist, then:

  • trying to read the data from a JavaScript script will return null.
  • trying to read the data from a Qworum script will raise a * reference fault at run-time.

3.1.2. Writing to a data container

Session data can be written by JavaScript scripts that run in Web pages:


// A method of the shopping cart service is initialising the state of
// a shopping cart Qworum object.
await Qworum.setData( ['@', 'line items'], Json([]) );

// This throws a TypeError exception if there is no Qworum object with path
// ['path','to','non-existent object'].
await Qworum.setData( ['path','to','non-existent object','data'], Json({}) );
        
Figure 3.1.2.1: Writing data to a data container.

Session data can also be written from within Qworum scripts, as shown in figure 3.1.1.2. Note that if the container's path refers to a Qworum object that does not exist, then the write attempt will raise a * reference fault at run-time.

3.2. Call

The Call instruction initiates a multi-phase service call. These calls are method calls in the OOP sense:

  • The first call argument is the path of the object that owns the call within the execution state. An object path is an array of strings (some string values are reserved).
  • The second call argument is the URL of the call's first phase. The default value is the current URL.

const aCall = Call( ['@', 'shopping cart'], 'view/' );
        
Figure 3.2.1: A Call instruction.

Some of the Faults that service calls can raise are * origin and * reference.

Service calls generate an HTTP(S) GET request when evaluated by the browser.

Service calls can have data arguments and/or object arguments.

3.2.1. Calls with data arguments

Service calls can have named data arguments, each argument containing one instruction or data. If a data argument contains an instruction, then the instruction will be evaluated before executing the call.

Data arguments will be made available to the new call on the client side as local data with the same names as the data arguments.


const 
lineItems = [{
  'title'   : 'Classic ankle boots',
  'price'   : {'EUR': 29.99},
  'quantity': 1
}],

aCall = Call(
  ['@','shopping cart'], '/add-line-items/',
  [
    {name: 'line items', value: Json(lineItems)}
  ]
);
      
Figure 3.2.1.1: A Call instruction with a data argument.

3.2.2. Calls with object arguments

Service calls can receive Qworum objects as arguments. These object arguments will be made available to the new call on the client side as local objects that have the same names as the object arguments.

This mechanism allows calls to have access to objects that would have been out of scope otherwise.


const 
lineItems = [{
  'title'   : 'Classic ankle boots',
  'price'   : {'EUR': 29.99},
  'quantity': 1
}],

aCall = Call(
  ['@','shopping cart'], '/add-line-items/',

  [ // data arguments
    {name: 'line items', value: Json(lineItems)}
  ],

  [ // object arguments
    {name: 'logger', object: ['@','logger']}
  ]
);
      
Figure 3.2.2.1: A Call instruction that contains an object argument.

3.3. Return

The Return instruction ends the current multi-phase service call by returning data. This instruction has one argument, which is an instruction or a data container.


const
aReturn = Return(
  Data(['@','line items'])
);
        
Figure 3.3.1: Returning data from the current call.

If the current call is the main call of the current browser tab, then this instruction will terminate the tab's execution.

3.4. Goto

The Goto instruction starts a new phase for the current multi-phase service call. It takes the URL of the next phase as argument.


const 
goto1 = Goto('phase-2.html'),
goto2 = Goto(); // go to the current URL
        
Figure 3.4.1: Goto instructions.

The Goto instruction will raise an * origin Fault if the object that the current call belongs to has a different origin ♦︎ than this Goto phase's origin. This is because all phases of all service calls belonging to an object must have the same origin URL.

3.5. Sequence

The Sequence instruction contains one or more instructions or data elements, each of which is evaluated in turn. This instruction will yield the evaluation result of the last instruction/data in the sequence.


// A shopping cart service directs the user to a remote payment processor.
const seq = Sequence(
  // 1. Call the payment processing service
  // 2. Store the returned transaction details.
  Data(
    ['@','latest transaction'],

    Call(
      ['@','payment service'], 'https://payment-processor.example/pay/',
      [{
        name: 'amount to pay',
        value: Json(
          {
            "amount"  : 98.99,
            "currency": "EUR",
          }
        )
      }]
    )
  ),

  // 3. Empty the shopping cart, because payment has succeeded (otherwise a fault would have been raised).
  Data( ['@','line items'], Json([]) ),

  // 4. Go to the next phase of the current shopping cart method.
  Goto('paid.html')
);
        
Figure 3.5.1: A Sequence example.

The sequence instruction will not yield a result in the following cases:

  • the sequence contains a Goto (which ends the evaluation of the Qworum script), or
  • a Fault is raised when evaluating one of the sequence's instructions, or
  • the sequence contains a Fault instruction.

3.6. Fault

In computer programming, exceptions are used for disrupting the normal course of execution, because an exceptional event has occurred which prevents the program to proceed as intended. Exceptions are called "faults" in Qworum parlance.

The Fault instruction has a fault-type argument which defaults to * service-specific. The specified value must not be a platform fault.


const aFault = Fault('payment cancelled');
        
Figure 3.6.1: A fault.

The type attribute's value has the following equivalent forms:

  • Case-insensitive — payment cancelled and Payment Cancelled are equivalent.
  • Separator whitespace is collapsed into one space character — payment cancelled and payment   cancelled are equivalent.
  • Whitespace at the beginning and the end are ignored — payment cancelled and     payment cancelled    are equivalent.

Faults are split into two categories:

  • Platform faults are faults whose type value starts with *. These can only be raised by the Qworum runtime itself. This means that a Qworum script must not contain a Fault instruction whose type is explicitly specified as being a platform fault.
  • Service-specific faults are faults that are defined by the Qworum services themselves for their specific use-cases (Example: login aborted ). Qworum scripts are allowed to contain and explicitly raise such faults. The type value of such faults must not start with *.

3.6.1. Fault types

Here is a visual overview of the predefined faults:

Figure 3.6.1.1: Qworum fault hierarchy.

Service-specific faults:

Qworum services can define their own service-specific fault types, which will be subtypes of * service-specific.

In addition, this specification defines the following service-specific fault subtypes, which can be raised by any service call:

  • arg faults indicate that a call argument was non-conformant or absent.
  • cancelled faults indicate that the end-user has terminated the call before it could yield a value.
  • internal faults indicate an internal error that occurred in a Qworum service during a service call. These faults do not imply any malfunction on the part of the Qworum platform itself.

Platform faults:

  • * entitlement — Raised when a Qworum service tries to use a Qworum feature that it isn't entitled to. These faults are typically raised when evaluating a Call instruction.
  • * origin — Raised when a phase of a Qworum object does not have the same origin ♦︎ as the Qworum object itself.
  • * platform fault in script — Raised when an XML Qworum script contains a platform fault. Note that this fault isn't raised for Qworum scripts that are generated in web pages using the official Qworum JavaScript library, in which case a TypeError is thrown instead.
  • * platform entitlement — Raised when a service that is being called isn't part of Qworum's Service Web. This may be because the service is not subscribed to a Qworum platform plan, or the current subscription isn't sufficient and needs to be upgraded.
  • * reference — Raised when the path of a data container or Qworum object can't be resolved.
  • * runtime — Raised when an unexpected error occurs in Qworum's browser runtime.
  • * script — Raised when a Qworum script is non-conformant.
  • * service — Parent type of all "user space"♦︎ faults that are caused by a Qworum service in a Qworum session.
  • * service entitlement — The service that is being called is a paid service, and the caller isn't subscribed to the called service.
  • * service-specific — The parent type of all service-specific faults.
  • * syntax — Raised when a Qworum script has syntax errors.
  • * user agent — Parent type of all "kernel space"♦︎ faults that occur in the user agent.

Note that there must be whitespace after *.

The browser extension that is currently available on Chrome Web Store isn't fully compliant, as it assumes that the platform fault identifiers do not start with a *. The upcoming extension release will fix this.

3.7. Try

When an instruction in a Qworum script raises a Fault, then the current call will terminate by bubbling up the fault to the caller instead of returning data, unless the fault is caught by a Try instruction.

The Try instruction has two arguments:

  1. The first argument is the instruction or data value to evaluate.
  2. The second argument is an array of catch clauses. Each clause specifies which faults to catch (an empty array means "all faults") and one instruction or data value to evaluate.

This instruction will yield the data value that was yielded by the evalution of the first argument or of a catch clause. Any uncaught fault will bubble up.


const
// Yield the value of a data container; initialise it if needed.
aTry = Try(
  Data(['shopping cart line items']),
  [
    {
      catch: ['* reference'], 
      do:    Data( ['shopping cart line items'], Json([]) )
    }
  ]
);
        
Figure 3.7.1: A Try instruction that initialises a data container.

The Try implementation is buggy in the browser extension that is currently available on Chrome Web Store. The upcoming extension release will fix this.

4. Data values

JSON and RDF are both natively supported.

4.1. Json

Represents a JSON-encoded data value ♦︎.


const aJson = Json(
  {
    "productID": "2",
    "name"     : "XYZ Boots",
    "offers"   : {
      "price"        : "75.95",
      "priceCurrency": "EUR"
    }
  }
);
        
Figure 4.1.1: Example of a Json data value.

4.2. SemanticData

Represents an RDF data value♦︎. This data can be specified in one of the following formats:


const 
// Create an empty data value.
dataValue = SemanticData(),

// Add RDF statements to the data value.
baseIRI       = new URL('https://site.example/turtle-file.ttl'),
turtleContent = `
PREFIX : <https://schema.org/>

[]
  a :ItemList ;
  :itemListElement [
    a :Product ;
    :productID "2";
    :name "XYZ Boots";
    :offers [
      a :Offer ;
      :price "75.95";
      :priceCurrency "EUR"
    ]
  ].
`;
await dataValue.readFromText(turtleContent, baseIRI);
Figure 4.2.1: Creating a semantic data value from an RDF/Turtle file.

5. Workarounds for browser limitations

The developers of Qworum applications and services must observe some simple rules for their Qworum-based software to work properly on browsers. These rules are necessary because Qworum is implemented as a browser extension, and web browsers impose certain constraints on what extensions can do. One notable restriction is that extensions are not allowed to prevent the end-user from going back in the tab history.

The following programming constraints will prevent the application UI from going out of sync with the application's session state.

In HTML pages, use window.location.replace() for hyperlinks.


<!-- BUG -->
<a href="new_url">
  A hyperlink
<a/>
Figure 5.1.1: Incorrect way of implementing hyperlinks in a Qworum session.

<!-- Correct -->
<button onclick="window.location.replace('new_url')">
  A hyperlink
<button />
Figure 5.1.2: Correct way of implementing hyperlinks in a Qworum session.

5.2. Forms

In HTML pages, use AJAX rather than web forms for sending data to servers.


<!-- BUG -->
<form action="/my-handling-form-page" method="post">
  <ul>
    <li>
      <label for="name">Name:</label>
      <input type="text" id="name" name="user_name" />
    </li>
    <li>
      <label for="mail">E-mail:</label>
      <input type="email" id="mail" name="user_email" />
    </li>
    <li>
      <label for="msg">Message:</label>
      <textarea id="msg" name="user_message"></textarea>
    </li>
    <li class="button">
      <button type="submit">Send your message</button>
    </li>
  </ul>
</form>
Figure 5.2.1: Incorrect way of sending forms during a Qworum session.

<!-- Correct -->
<ul>
  <li>
    <label for="name">Name:</label>
    <input type="text" id="name" name="user_name" />
  </li>
  <li>
    <label for="mail">E-mail:</label>
    <input type="email" id="mail" name="user_email" />
  </li>
  <li>
    <label for="msg">Message:</label>
    <textarea id="msg" name="user_message"></textarea>
  </li>
  <li class="button">
    <button onclick="submit()">Send your message</button>
  </li>
</ul>
Figure 5.2.2: Correct way of sending forms during a Qworum session.

6. References