---
tags: hope
---
# Core API
This documents the design of an experimental UAE (Universal Application Environment) API, called 'hope'. The goal is to see if we can achieve a simple, intuitive API that allows us to build a real live universal document system on top.
## parts
- initialization
- bus
- connect
- pub/sub
- call/reply
- seamless
- resize content
- transfer styling
- reflection
- register api
- register api message
- list api's
- list message / calls
- storage
- register subsystem
- initialize subsystem
- subsystem api
- list
- fetch
- store
- move/rename
- copy
- delete
- search?
## minimal document
```
<script src="//example.com/app.js" type="module"></script><!--
```
A minimal HTML document might look like this:
```
<script type="module">
import { hope } from "/js/hope.js"
// application code here
</script>
<title>A minimal HTML document</title>
<h1>A minimal HTML document</h1>
<iframe name="subdocument"></iframe>
```
## initialization
This is started simply by importing the `hope.js` code. It sets up the UAE api and adds extra DOM elements:
```
document.hope.documents = {
<name>: <HopeDoc>{
name: "<name>",
frame: <iframe>,
status: <state:HopeDoc.LOADING|HopeDOc.CONNECTED|HopeDoc.FAILED>
editable: <bool>
}
};
```
It detects subdocuments (iframes for now) and sets up the bus connection, if possible. It also starts the local bus and subscribes to bus connect messages, so if it is embedded/hosted, it can setup a connection with the host.
## bus
### connect
```
bus.connect(iframe?)
.then(window => {...})
.catch(error => {...})
```
The iframe argument to connect may be an actual iframe element, the name of the iframe, or you may skip it. If you don't pass the iframe parameter, the bus will wait for a connection from the host document. If the connection is not setup within the global timeout (default 10 seconds) it will reject the promise.
### pub/sub
bus.publish('/api/message/', <params>, iframe)
bus.subscribe('/api/message/', <callback(event), iframe>)
bus.unsubscribe('/api/message', <callback>, iframe)
The pub/sub bus doesn't guarantee delivery. This is a fire and forget system. If the target is not ready yet, the bus.publish() call will finish and not throw an error. You must implement your own checks on top of this.
Similar the bus.subscribe callback simply won't be called if no suitable message arrives.
The bus.unsubscribe method removes a listener, but you must pass it the exact same callback method and iframe that you used with bus.subscribe.
### call/reply
```
bus.call('/api/message/', <params>, iframe)
.then(event => { ... })
.catch(error => { ... });
bus.subscribe('/api/message/', event => {
if (event.message.id) {
bus.reply(event, <reply>);
}
});
```
bus.call is an abstraction on top of bus.publish. It adds a unique message.id parameter to the message. Then adds a listener for the exact same message name on the target. Then sends the message just like bus.publish would. The listener checks each message for the message.replyTo parameter, which must be the same as the original message.id. If found, it resolves the promise, with the event.message as the only parameter. On timeout (default 10 seconds) the reject() part of the promise is called with a timeout error.
TODO: do error handling. Catch thrown errors in an api callback. Convert these to an error message in the reply. Then re-throw the error so a developer can fix it in the javascript debugger.
## seamless
### resize content
A seamless document is a iframe that sends signals to the host document to resize the iframe to fully contain that content of that iframe. This means you don't see borders or a scrollbar. A host may force a maximum height or width though.
A seamless document sets up a listener for seamless api requests:
```
bus.subscribe('/x/hope/seamless/request-size', event => {
bus.reply(event, {
size: {
width: <int>,
height: <int>
}
});
});
```
If you do not want an iframe to be included seamless, set the `data-hope-seamless="false"` attribute.
### transfer styling
A seamlessly included iframe should follow the styling of the host document if possible. If you change the font in the host document, it should also change in the iframe.
There are a few challenges here. The host doesn't know about the HTML structure of the iframe. However the author of the host document _may_ know that. So you could add iframe-specific style information in the host document. This is cumbersome however. It can be eased with a bit of standardization.
A simple first step is to define a basic style for the host document, that just styles HTML tags. This is a seperate CSS file. Only this file is passed on to embedded iframes. This allows a baseline for all embedded documents.
In addition we can use CSS custom properties, such as:
```
:root {
--hope-color-primary: blue;
--hope-color-support: cyan;
}
```
By setting up a base hope.css that includes these properties, all supporting documents could use these properties in their own CSS. If a host document changes these, the embedded documents can apply the styling to their own custom CSS:
```
HopeDoc.api('/x/hope/seamless/').setStyle(name, CSSString)
HopeDoc.api('/x/hope/seamless/').getStyle(name)
// -> CSSString
HopeDoc.api('/x/hope/seamless/').getStyles()
// -> [ ...name ]
```
The default style hope uses is named 'hope'.
## reflection
This part allows you to query child or host documents to see what API's they support. This is a necessary part needed to support a live IDE (Integrated Development Environment), similar to SmallTalk.
reflection:
- list API's
- list API Methods
- get API Method description, parameter and return value definitions
- add API
- add API Method(s)
```
HopeDoc.api('/x/hope/reflection/').list()
// => [ ...<API names> ]
```
```
HopeDoc.api('/x/hope/reflection/').list(<API name>)
// => [ ...<API messages>]
```
```
HopeDoc.api('/x/hope/reflection/').get(<API message>)
// => {
// message: "<API message name>",
// description: "...",
// params: {
// <name>: {
// type: <type>,
// nullable: <booL>
// description: "..."
// }
// },
// return: {
// type: <type>,
// description: "..."
// }
// }
```
The default description format should probably be markdown, as that is so well known. The problem is that it is underspecified and allows HTML to be included, which makes all kinds of problems possible. An alternative could be asciiDoc, which looks nice, but has only a compiled-from-ruby javascript parser/renderer. So for now, markdown with a html purifier is probably the best way to go.
```
let foo = document.hope.api.register('/x/foo/')
foo.bar = {
description: "...",
params: {
<anme>: {
type: <type>,
nullable: <bool>,
description: "..."
}
},
return: {
type: <type>,
description: "..."
},
callback: <function>
};
```
of
```
let foo = document.hope.api.register('/x/foo/bar/', {
description: "...",
params: {
<anme>: {
type: <type>,
nullable: <bool>,
description: "..."
}
},
return: {
type: <type>,
description: "..."
}
}, <function>);
```
## storage
Documents need to be stored somewhere, so it is best to add a common storage API with support for different subsystems. This way each document can in principle be stored on any kind of storage system.
Inspiration for this API is from a number of sources:
- [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)
- [remoteStorage](https://remotestorage.io/)
- [flysystem](https://flysystem.thephpleague.com/)
You should be able to add subsystems on the fly. This way your application and your IDE can add subsystems that you use regularly automatically, without the document having to know about them.
Using localStorage or the Filesystem API, you can have private documents that are only visible to you.
### register subsystem
Registering a storage subsystem means that you make it available for a user to select when opening or saving documents. The document itself doesn't need to know anything about the storage subsystem. A document may disallow saving to any subsystem.
```
document.hope.storage.register('MyAWS', () => myAWS.factory())
```
### initialize subsystem
A storage subsystem may require authentication before use. If so it should trigger its own UI for this when selected by the user. It can do this on the first call to `write()` or any other API call.
### subsystem api
A minimal subsystem only needs one method:
```
write()
```
But that will only allow you to save changes to the current document. You can't save a copy to a new location. There is no need for a `read()` method in the minimal API, as the document is already read and in memory.
Most storage solutions will have a filesystem-like abstraction. In that case the subsystem API can provide a default set of methods and the Hope IDE can use those to provide a standard file browser UI.
These are:
- list
- exists
- read
- write
- delete
- move/rename
- copy
- mkdir
- rmdir
A storage subsystem may add its own UI, e.g. for authentication and authorization. Each of the subsystem methods may trigger a popup or other UI. To allow for that, each of these methods are defined as async, that is they will always return a Promise.
### Examples
```
let subsystem = document.hope.storage.subsystem('myS3');
subsystem.write()
.then(() => {
// success...
})
.catch(error => {
// handle the error
})
```
If you don't pass any options to `write()` it is expected that it can extract the necessary information from the current document.