Object Notation / Notification (ON) | The Object Web

Duncan Cragg

This is the Observer Pattern for Objects in the Object Web. The ON model is of a single, universal pool of Objects aware of each other's state. An Object expresses its interest in another and, if permitted, can then read that peer's state. In addition, it is notified of changes in that peer's state; until it loses interest.

Objects are considered to be 'live', with four interface points: they can read and change their own state, they can read another's state, and they can be notified of another's change in state, including the fact that they're watching back...

ON is protocol-independent: i.e., it doesn't depend on the OP Object Protocol and needn't even see an Internet connection - ON Object Notification may occur in-process.

Object Notation

Objects are encoded into text. They are written in a very simple, YAML-like notation, using only indentation, space, colon and hyphen to generate structure.

The characters of Object text are Unicode and will always be serialised into UTF-8, and often stored and processed in UTF-8, too. Control characters are excluded apart from all forms of newline or vertical movement (CR, LF, CRLF, FF, NEL, etc) and the space. Tabs are thus excluded. If encountered on input, these excluded characters are converted to spaces. Newline or vertical control characters are identical to CRLF, and will appear normalised on output. Multiple spaces are identical to single spaces except where they're part of indentation, and will appear normalised as single spaces on output. There is no escaping apart from a literal colon. There is no Byte Order Mark. There may be NULL or EOF termination of Object text, depending on context.

Basic Object Notation is an ordered hashtable. Non-space sequences of characters are called symbols and are not case-sensitive, so capitals are normalised to lower case if found on input. Hashtable keys are symbols and thus cannot have spaces or be structured. Keys in a hashtable must be unique, so there is also a list notation within the ordered hash: a special key written as a hyphen, repeated for each element of the list.

Here's what it looks like:

owid: 23423-4141a
open: -: owid: ffda0-582ce
         mode: silent
      -: owid: 11ab3-001a3
         mode: deny

here: nested: hash
      unicode: £1234.50
      and: -: another
           -: list: and
              hash: again
there: content

Scalar types such as int or float are not defined here, but may be defined in OR Object Rules. There is no concept of 'null', apart from an empty item, which may be represented internally as any of empty string, empty hashtable or empty list or directly as an internal null. There is no notation for comments defined here, but it may be defined in OR Object Rules.

It should be understood that this is a structured symbol notation for data only: Object Notation is explicitly not meant for textual or document data. There are no literal quoted strings, and it doesn't allow tunnelling of textual structures such as escaped tabs and newlines. Documents may, of course, be constructed from structures of strings, including Wiki-like markup for capitalisation, bold, italic, etc, and structures representing paragraphs and tables.

Horizontal and Vertical Lists

A horizontal sequence of symbols separated by spaces is identical to a vertical list of symbols indicated by the hyphen/colon. The list elements may be wrapped with indentation - the newline and indentation at the wrap is the same as a single space, separating the symbols of the list:

here: is a long list of symbols
      that wraps and wraps and
      goes on until...
it: stops! which
    then equals..
this: -: stops!
      -: which
      -: then
      -: equals..
andthis: stops!
         which
         then
         equals..

There is no way to actually get newlines (or any other control characters including spaces) into symbols or lists of symbols. So "multiline strings" have to be represented as a list of lists of symbols.

In the same way that horizontal spaces create lists of symbols, empty lines vertically create a top-level list of hashtables thus separated. Multiple empty lines are collapsed:

hash: table
with: entries

-: next hash table (a list!)
-: with more entries

which: equals this on its own..

-: hash: table
   with: entries
-: -: next hash table (a list!)
   -: with more entries

Note that it is not possible to have a vertical list that includes symbols or horizontal lists of symbols. This rule may only be relaxed by allowing a vertical list of hashtables that is ended with a final horizontal list of symbols. The reason for this, apart from conceptual simplicity (outer vertical is only hashes, inner horizontal is only symbols), is that it allows a space- and CPU-saving internal linked list representation, where hashes have pointers to the next vertical element and symbols have pointers to the next horizontal element.

Symbol as Single-Element List or Hash Key

A single symbol could become a list with itself as the first element, if a neighbour is added. Similarly, a single symbol could become a hash key:

a: b       -- single symbol
a: b c     -- list [ b c ]
a: -: b
   -: c    -- list [ b c ]
a: b:      -- hash
a: b: c    -- hash
   d:

This shape-changing in the symbol 'b' comes out when using path expressions to navigate Object Notation...

Path Expressions

An Object Notation tree can be navigated with path expressions. A path expression returns the sub-tree found at some point, the empty string if the path ends there, or null if the path doesn't exist.

For example, the path expression 'a:b' returns these results for the above:

a: b       -- path a:b = ""; a:c = null
a: b c     -- path a:b = "" which also = a:-:b
a: -: b    -- path a:b = "" which also = a:-:b
   -: c    -- path a:c = "" which also = a:-:c
a: b:      -- path a:b = ""
a: b: d    -- path a:b = d; a:c = ""
   c:

An empty string, empty list and empty hashtable are all equivalent. The consequence of this is that you navigate by symbol rather than structure type. The one variant on this is that lists may be indexed:

a: b c     -- path a:b = "" = a:-:b = a:1:b
a: -: b    -- path a:b = "" = a:-:b = a:1:b
   -: c    -- path a:c = "" = a:-:c = a:2:c

Since this is an external represention, the first element of a list is at index 1.

Headers and Content

Objects have at least two parts in a top-level, empty-line-separated list. The two parts are the headers and the content:

owid: 23423-4141a

this: is the content

Objects have an Object Web ID or OWID which is always present in the headers and is immutable. OWIDs may or may not be UUIDs, but must be globally unique identifiers that are usually constructed from hyphen-separated groups of hexadecimal digits. The headers look much like HTTP, and will share some HTTP semantics here and in the OP Object Protocol.

Both header and content parts follow this same basic Object syntax. The 'Content-Type' of the content part is always a fragment of OR Object Rules notation, which extends Object Notation. In this specification, nothing much is said about the OR content part, only the headers.

View Events

An Object may subscribe to, observe, watch, access or view another, or many others:

owid: 23423-4141a
view: -: owid: ffda0-582ce
      -: owid: 11ab3-001a3

this: is the content

Which sends the following View Event to ffda0-582ce:

owid: 23423-4141a
view: -: owid: ffda0-582ce

The View Event looks like a reduced version of the viewer Object, with only the relevant parts announced.

When it does this, the target automatically gets a corresponding entry, called a Cast, after multicast:

owid: ffda0-582ce
cast: -: owid: 99a27-5510c
      -: owid: 23423-4141a
      
stuff: content

Casts are immutable as far as the Object is concerned.

Permission

However, the target of a View needs to set permissions to allow itself to be seen by its peers, with Open entries:

owid: ffda0-582ce
cast: -: owid: 23423-4141a
      -: owid: 99a27-5510c
open: -: owid: 23423-4141a
         mode: silent

stuff: content

It's only allowing 23423-4141a to view itself, and doesn't want to be notified of any change in that fact: with 'silent', the permission is executed silently - a peer can View this one without it knowing, without it seeing the View Event above. The default is to report all View Events, and if there's no Open entry, the attempt is reported but nothing is returned.

The OWID can be set to '*' to set permissions for all peers. Only individual OWIDs or '*' can be used, there's no way to specify groups any other way. OP will introduce host-level permissions, however.

Cast Events

When a target adds an Open entry affecting a Viewer, or if a View is first set up and there's a corresponding Open entry already there, the Viewers are notified with a Cast Event.

Here is the Cast Event that 23423-4141a sees - without all the fields it's not interested in, are obvious or it's not allowed to see:

owid: ffda0-582ce
cast: -: owid: 23423-4141a

stuff: content

The 23423-4141a Object can continue to access this state at any time, and again will only see this cut-down view.

Denial

Maybe this target wants to deny 99a27-5510c without being bothered by it any more:

owid: ffda0-582ce
cast: -: owid: 23423-4141a
      -: owid: 99a27-5510c
open: -: owid: 23423-4141a
         mode: silent
      -: owid: 99a27-5510c
         mode: deny silent

stuff: content

If a peer is denied, it sees a denial Cast Event, like this:

owid: ffda0-582ce
cast: -: owid: 99a27-5510c
open: -: owid: 99a27-5510c
         mode: deny

Again, the fact that it is silent is removed as none of its business.. Notice that permitted peers don't get any Open entry reported in this way, as it's obviously allowed.

The following will default to initial automatic denial for all-comers (apart from the specific, overriding case of 23423-4141a), but giving notification to ffda0-582ce of each attempt anyway:

owid: ffda0-582ce
cast: -: owid: 23423-4141a
      -: owid: 99a27-5510c
open: -: owid: 23423-4141a
         mode: silent
      -: owid: *
         mode: deny

stuff: content

Other Details

Nothing is returned until an Open entry covering the Cast entry exists - either allow or deny. If deny, the Cast entry is dropped as soon as the deny Cast Event is returned.

Without an entry or with an entry that isn't 'silent', all View attempts are reported. If set just to 'deny', without 'silent', this Object will be told of every attempt by 99a27-5510c to access it just as if no entry existed, but the denial Cast Event will be immediate and automatic each time.

In ON, header information always takes the place of notifying what in HTTP would be in the status code. The above permitted Cast Event is like a 200 OK, and the deny Cast Event is like a 403 Forbidden, for example.

Updating Object Headers and Content

The state of an Object may be updated in a number of ways: setting or removing View and Open entries, and making changes to the private or public content.

To re-poll or re-sample, a View entry may be re-added, causing another View Event to be sent to the target, which may not be seen by it if set to 'silent'. If an Open entry exists, whether deny or not, a Cast Event is automatically returned. If the View entry is removed, the corresponding Cast entry is also removed, and no more updates are sent.

Similarly, re-setting an Open entry will re-send the Cast Event, even if the content hasn't actually changed. Removing an Open entry will stem the updates, without notifying the Viewer, and will drop the Cast entry. Setting an Open entry to deny will send a deny Cast Event, causing the Cast entry to then be dropped. Removing deny will have no effect, as there could not have been any corresponding Cast entries.

When the public content is updated, the new state is sent to Viewers in the Cast list as before.

Private state

Maybe ffda0-582ce is thinking about 99a27-5510c. Objects have private state to store such intermediate data:

owid: ffda0-582ce
cast: -: owid: 23423-4141a
      -: owid: 99a27-5510c
open: -: owid: 23423-4141a
         mode: silent
priv: think-about: 99a27-5510c

stuff: content

Private data is any valid Object Notation content.

Maybe this Object wants to take a look back itself at 99a27-5510c, to help it decide what to do with its outstanding request:

owid: ffda0-582ce
view: -: owid: 99a27-5510c
cast: -: owid: 23423-4141a
      -: owid: 99a27-5510c
open: -: owid: 23423-4141a
         mode: silent
priv: think-about: 99a27-5510c

stuff: content

View before Definition

If an Object is undefined at the time of Viewing, an empty Object is created:

owid: 90af2-122bd
cast: -: owid: 23423-4141ab
      -: owid: 580ac-dd09a0

with no content yet, and all pending clients listed. When the state is defined, including Opens, the Cast events are sent out.

If the Object defines itself like this:

owid: 90af2-122bd
open: -: owid: 23423-4141ab
      -: owid: 580ac-dd09a0

got here in the end..

It then looks like this:

owid: 90af2-122bd
cast: -: owid: 23423-4141ab
      -: owid: 580ac-dd09a0 
open: -: owid: 23423-4141ab
      -: owid: 580ac-dd09a0

got here in the end..

generating these two events at the same time:

owid: 90af2-122bd
cast: -: owid: 23423-4141ab

got here in the end..


owid: 90af2-122bd
cast: -: owid: 580ac-dd09a0

got here in the end..

Unsolicited Cast Events - Postable Objects

An Object can send its state to a peer without being asked for it with a prior, explicit View. However, there must be a view: -: owid: * at this target, meaning this Object is 'postable' or open to unsolicited events like these - not that it wants to be omniscient!

The sender simply adds an Open entry to the target, which automatically adds a Cast entry and sends the Cast Event. Luckily, an Object that is made globally Open by setting open: -: owid: * doesn't get its state pushed into every such 'postable' peer Object in the Universe!

Others can also see this state if allowed, so the Cast Event is not personalised to the target, unless this Object is Open only to the target and none other.

Duncan Cragg, July 2008
[2278]