🚧 This documentation is for the developer preview of m-ld.
The Javascript engine can be used in a modern browser or a server engine like Node.js.
The Javascript clone engine conforms to the m-ld specification. Its support for transaction patterns is detailed below. Its API concurrency model is based on serialised consistent states.
The live code app below creates a domain of shared information, and then follows updates to it. In its user interface, it just presents the domain name. (You can see and edit the code by clicking the button, left-middle.)
import { clone, uuid } from 'https://js.m-ld.org/ext/index.mjs';
import { MemoryLevel } from 'https://js.m-ld.org/ext/memory-level.mjs';
import { IoRemotes } from 'https://js.m-ld.org/ext/socket.io.mjs';
const domainName = `${uuid()}.public.gw.m-ld.org`;
const state = await clone(new MemoryLevel(), IoRemotes, {
'@id': uuid(),
'@domain': domainName,
genesis: true,
io: { uri: "https://gw.m-ld.org" }
});
successDiv.removeAttribute('hidden');
domainInput.value = domainName;
for await (let [update] of state.follow()) {
for (let { name } of update['@delete'])
document.getElementById(`welcome_${name}`).remove();
for (let { name } of update['@insert'])
successDiv.insertAdjacentHTML('beforeend',
`<h2 id="welcome_${name}" class="bounce">Welcome, ${name}!</h2>`);
}
<div id="successDiv" hidden>
<p>🎉 Your new domain is at</p>
<input id="domainInput" type="text" onfocus="this.select()" style="width:100%;"/>
<hr/>
<p><i>When you've cloned the domain below, check back here for updates...</i></p>
</div>
@keyframes bounce {
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-30px); }
60% { transform: translateY(-15px); }
}
.bounce {
animation: bounce 1s;
}
The new domain's information is stored in memory here (and only here). The next live code app, below, allows us to make another clone of the domain. These two code blocks are sandboxed – they do not have access to each other's state via this browser.
💡 You can confirm this by trying it in another window (use the button, top right), another browser, or another device... anywhere in the world!
import { clone, uuid } from 'https://js.m-ld.org/ext/index.mjs';
import { MemoryLevel } from 'https://js.m-ld.org/ext/memory-level.mjs';
import { IoRemotes } from 'https://js.m-ld.org/ext/socket.io.mjs';
cloneButton.addEventListener('click', async () => {
const state = await clone(new MemoryLevel(), IoRemotes, {
'@id': uuid(),
'@domain': domainInput.value,
io: { uri: 'https://gw.m-ld.org' }
});
playgroundAnchor.setAttribute('href', `https://m-ld.org/playground/#domain=${domainInput.value}&txn=%7B%22name%22%3A%22George%22%7D`);
clonedDiv.removeAttribute('hidden');
addNameButton.addEventListener('click', () =>
state.write({ name: nameInput.value }));
rmNameButton.addEventListener('click', () =>
state.write({ '@delete': { name: nameInput.value } }));
});
<div>
<p>Paste the domain name here:</p>
<input id="domainInput" type="text" style="width:100%;"/>
<button id="cloneButton">Clone</button>
</div>
<div id="clonedDiv" hidden>
<p>🎉 You have cloned the domain!</p>
<p>Please enter your name:
<input id="nameInput" type="text"/>
<button id="addNameButton">Add</button>
<button id="rmNameButton">Remove</button>
</p>
<p>You can also interact with this domain in the <a id="playgroundAnchor" target="_blank"><b>m-ld</b> playground</a>!</p>
</div>
The domain is using a public Gateway (gw.m-ld.org) to connect clones together. This means the m-ld playground can also see it.
💡 m-ld domain names look like IETF internet domains, and have the same rules. The internet doesn't know how to look them up yet though, so you can't just paste one into a browser.
There are more live code examples for you to try in the How-To section below.
In Node.js, or using a bundler, use:
npm install @m-ld/m-ld
– latest stable prereleasenpm install @m-ld/m-ld@edge
– bleeding edgeBrowser bundles (as used above) are served on:
js.m-ld.org/ext
– latest stable prereleaseedge.js.m-ld.org/ext
– bleeding edgeThey include the core (index.mjs
), bundled remotes (mqtt.mjs
, ably.mjs
and socket.io.mjs
), in-memory backend (memory-level.mjs
) and other extensions.
Some example starter projects available:
m-ld uses abstract-level to interface with a LevelDB-compatible storage backend.
A m-ld clone uses a 'remotes' object to communicate with other clones.
MqttRemotes
.AblyRemotes
.IoRemotes
.💡 If your architecture includes some other publish/subscribe service like AMQP or Apache Kafka, or you would like to use a fully peer-to-peer protocol, please contact us to discuss your use-case. Remotes can even utilise multiple transport protocols, for example WebRTC with a suitable signalling service.
The clone function initialises the m-ld engine with a leveldb back-end and the clone configuration.
import { clone, uuid } from '@m-ld/m-ld';
import { MemoryLevel } from 'memory-level';
import { MqttRemotes, MeldMqttConfig } from '@m-ld/m-ld/ext/mqtt';
const config: MeldMqttConfig = {
'@id': uuid(),
'@domain': 'test.example.org',
genesis: true,
mqtt: { hostname: 'mqtt.example.org' }
};
const meld = await clone(new MemoryLevel, MqttRemotes, config);
The clone
function returns control as soon as it is safe to start making data transactions against the domain. If this clone has been re-started from persisted state, it may still be receiving updates from the domain. This can cause a UI to start showing these updates. If instead, you want to wait until the clone has the most recent data, you can add:
await meld.status.becomes({ online: true, outdated: false });
MQTT is a machine-to-machine (M2M)/"Internet of Things" connectivity protocol. It is convenient to use it for local development or if the deployment environment has an MQTT broker available. See below for specific broker requirements.
The MqttRemotes
class and its companion configuration class MeldMqttConfig
can be imported or required from '@m-ld/m-ld/ext/mqtt'
. You must also
install the async-mqtt
package as a peer of @m-ld/m-ld
.
A bundle is also available at http://js.m-ld.org/ext/mqtt.mjs
.
The configuration interface adds an mqtt
key to the base
MeldConfig
. The content of this key is a client
options object for MQTT.js. It must
not include the will
and clientId
options, as these are set internally. It
must include a hostname
or a host
and port
, e.g.
const config = {
'@id': uuid(), '@domain': 'test.example.org', genesis: true,
mqtt: { host: 'localhost', port: 1883 }
};
MqttRemotes
requires broker support for:
A good choice for local development is Aedes.
MQTT remotes supports websockets for use in a browser environment. To configure,
add protocol: 'ws'
(or 'wss'
) to the mqtt
configuration value. (Note that
All the MQTT configuration goes through the mqtt
key, even if it's actually
using websockets for transport.) This requires the MQTT broker to support
websocket connections, for example see the
Aedes documentation.
Ably provides infrastructure and APIs to power realtime experiences at scale. It is a managed service, and includes pay-as-you-go developer pricing. It is also convenient to use for global deployments without the need to self-manage a broker.
The AblyRemotes
class and its companion configuration class MeldAblyConfig
can be imported or required from '@m-ld/m-ld/ext/ably'
. You must also
install the ably package as a peer of @m-ld/m-ld
.
A bundle is also available at http://js.m-ld.org/ext/ably.mjs
.
The configuration interface adds an ably
key to the base
MeldConfig
. The content of this key is an Ably
client options
object. It
must not include the echoMessages
and clientId
options, as these are set
internally.
If using token
authentication,
ensure that the clientId
the token is generated for corresponds to the @id
given in the MeldConfig
.
Socket.IO enables real-time, bidirectional and event-based communication. It works on every platform, browser or device, focusing equally on reliability and speed. It is convenient to use when the app architecture has a live web server or app server, using HTTP.
The IoRemotes
class and its companion configuration class MeldIoConfig
can be imported or required from '@m-ld/m-ld/ext/socket.io'
. You must also
install the socket.io-client
package as a peer of @m-ld/m-ld
.
A bundle is also available at http://js.m-ld.org/ext/socket.io.mjs
.
The configuration interface adds an io
key to the base
MeldConfig
. The value is an optional object
having:
uri
: The server URL (defaults to the browser URL with no path)opts
: A
Socket.io factory options
object, which can be used to customise the server connectionWhen using Socket.io, the server must correctly route m-ld protocol
operations to their intended recipients. The Javascript engine package bundles a
class for Node.js servers, IoRemotesService
, which can be imported
from '@m-ld/m-ld/ext/socket.io/server'
.
To use, initialise the
Socket.io server as normal, and then construct an IoRemotesService
, passing
the namespace you want to make available to m-ld. To use the global
namespace, pass the sockets
member of the Server
class. For example:
const socket = require('socket.io');
const httpServer = require('http').createServer();
// Start the Socket.io server, and attach the m-ld message-passing service
const io = new socket.Server(httpServer);
new IoRemotesService(io.sockets);
For a complete example, see the web starter project .
For other server types, contact us.
A m-ld transaction is a json-rql pattern, which represents a data read or a data write. See the m-ld specification for a walk-through of the syntax.
Supported pattern types for this engine are (follow the links for examples):
💡 If you have a requirement for an unsupported pattern, please contact us to discuss your use-case. You can browse the full json-rql syntax at json-rql.org.
Subjects in the Javascript engine are accepted and presented as plain Javascript objects whose content is JSON-LD (see the m-ld Specification). Utilities are provided to help the app use and produce valid subjects.
Clone updates obtained from a read handler specify the exact Subject property values that have been deleted or inserted during the update. Since apps often maintain subjects in memory, for example in a user interface, utilities are provided to help update these in-memory subjects based on updates:
A m-ld clone contains realtime domain data in principle. This means that any clone operation may be occurring concurrently with operations on other clones, and these operations combine to realise the final convergent state of every clone.
The Javascript clone engine API supports bounded procedures on immutable state, for situations where a query or other data operation may want to operate on a state that is unaffected by concurrent operations. In general this means that in order to guarantee data consistency, it is not necessary for the app to use the clone's local clock ticks (which nevertheless appear in places in the API for consistency with other engines).
An immutable state can be obtained using the read and write methods. The state is passed to a procedure which operates on it. In both cases, the state remains immutable until the procedure's returned Promise resolves or rejects.
In the case of write
, the state can be transitioned to a new state from within
the procedure using its own write method,
which returns a new immutable state.
In the case of read
, changes to the state following the procedure can be
listened to using the second parameter, a handler for new states. As well as
each update in turn, this handler also receives an immutable state following the
given update.
await clone.read(async (state: MeldReadState) => {
// State in a procedure is locked until sync complete or returned promise resolves
let currentData = await state.read(someQuery);
populateTheUi(currentData);
}, async (update: MeldUpdate, state: MeldReadState) => {
// The handler happens for every update after the proc
// State in a handler is locked until sync complete or returned promise resolves
updateTheUi(update); // See §Handling Updates, below
});
ui.on('action', async () => {
clone.write(async (state: MeldState) => {
let currentData = await state.read(something);
let externalStuff = await doExternals(currentData);
let todo = decideWhatToDo(externalStuff);
// Writing to the current state creates a new live state
state = await state.write(todo);
await checkStuff(state);
});
});
ui.on('show', () => {
clone.read(async (state: MeldReadState) => {
let currentData = await state.read(something);
showTheNewUi(currentData);
});
});
Here is a roll-up of links to usage docs and live coding examples to help get started with common patterns.
import { clone, uuid } from 'https://js.m-ld.org/ext/index.mjs';
import { MemoryLevel } from 'https://js.m-ld.org/ext/memory-level.mjs';
import { IoRemotes } from 'https://js.m-ld.org/ext/socket.io.mjs';
// m-ld extensions are loaded using their package identity (@m-ld/m-ld/ext/..).
// In a real app, this redirection should be done with an import map.
globalThis.require = module => import(module
.replace(/@m-ld\/m-ld\/ext\/(\w+)/, 'https://js.m-ld.org/ext/$1.mjs'));
globalThis.changeDomain = async function (domain) {
const genesis = !domain;
if (genesis)
domain = `${uuid()}.public.gw.m-ld.org`;
if (window.model) {
await window.model.state.close();
delete window.model;
}
const state = await clone(new MemoryLevel(), IoRemotes, {
'@id': uuid(),
'@domain': domain,
genesis,
io: { uri: "https://gw.m-ld.org" }
});
// Uncomment the next line to log individual updates as they come in
//state.follow(update => console.info('UDPATE', JSON.stringify(update)));
domainInput.value = domain;
appDiv.hidden = false;
playgroundAnchor.setAttribute('href', `https://m-ld.org/playground/#domain=${domain}`);
// Store the "model" as a global for access by other scripts, and tell them
window.model = { domain, state, genesis };
document.dispatchEvent(new Event('domainChanged'));
}
/**
* Utility to populate a template. Returns an object containing the cloned
* children of the template, also indexed by tagName and classname.
*/
globalThis.templated = template => new Proxy({ $: template.content.cloneNode(true) }, {
get: (t, p) => t[p] ?? t.$.querySelector(p) ?? t.$.querySelector(`.${p}`)
});
document.querySelectorAll('.help').forEach(help => helpDetails.appendChild(templated(help).$));
<div>
<a id="playgroundAnchor" target="_blank" title="go to playground">🛝</a>
<input id="domainInput" type="text" placeholder="domain name" onfocus="this.select()"/>
<button onclick="changeDomain(domainInput.value)">Join</button>
<button onclick="changeDomain()">New ⭐️</button>
<details id="helpDetails">
<summary>🔢 help...</summary>
<p>This live code demo shows how to share live information with <b>m-ld</b>.</p>
<p>To get started with a new set of information (a "domain"), click New ⭐️ above. You can then interact with the mini-application below to create some information.</p>
<p>To share the information with a duplicate of this page:<ol><li>copy the domain name above</li><li>duplicate the browser tab</li><li>paste the domain name into the new page's domain input</li><li>click Join</li></ol></p>
<p>You can also share with the <b>m-ld</b> playground using the 🛝 button.</p>
</details>
<hr/>
</div>
These examples show simple patterns for getting started with an app's code structure. In principle, m-ld acts as a "model", replacing (or being proxied by) the local in-memory data model. Because m-ld information is fundamentally live – it can change due to a remote edit as well as a local one – it's valuable for the local app code to react to changes that it may not have initiated itself.
import { shortId } from 'https://js.m-ld.org/ext/index.mjs';
answerButton.addEventListener('click', async () => {
const { state } = window.model;
const question = questionInput.value;
if (question) {
const knowledgeId = shortId();
await state.write({ '@id': knowledgeId, question });
const botAnswer = await askBot(question);
if (botAnswer)
state.write({ '@id': knowledgeId, answer: botAnswer });
}
});
async function askBot(question) {
const response = await fetch(
`https://runkit.io/gsvarovsky/question/0.1.2?q=${encodeURIComponent(question)}`);
return response.text();
}
<div id="appDiv" hidden>
<h2>Collaborative Knowledge Base</h2>
<p>
<label for="questionInput">Ask anything!</label>
<input id="questionInput" type="text" value="Who is Elvis?"/>
<button id="answerButton">Answer</button>
</p>
<hr/>
<template id="knowledgeTemplate">
<div class="knowledgeDiv">
<p class="questionText"></p>
<ul class="answerList"></ul>
<input class="answerInput" type="text" placeholder="Your answer"/>
<button class="addAnswerButton">Add</button>
</div>
</template>
<div id="knowledgeBaseDiv"></div>
</div>
import { array } from 'https://js.m-ld.org/ext/index.mjs';
document.addEventListener('domainChanged', async () => {
window.model.state.read( // 1️⃣
// Populate the UI as soon as the domain has been changed.
state => reloadAll(state),
// In this example, we just refresh the whole UI state on every update.
(update, state) => reloadAll(state)
)
});
async function reloadAll(state) {
knowledgeBaseDiv.innerHTML = '';
return state.read({ // 2️⃣
'@describe': '?id', '@where': { '@id': '?id' } // 3️⃣
}).each(({ '@id': kid, question, answer }) => { // 4️⃣
const {
knowledgeDiv, questionText, answerList, answerInput, addAnswerButton
} = templated(knowledgeTemplate);
questionText.innerText = question;
// The `answer` field can be one string or an array
for (let ans of array(answer))
answerList.insertAdjacentHTML('beforeend', `<li>${ans}</li>`);
addAnswerButton.addEventListener('click', () => {
answerInput.value && window.model.state.write({ // 5️⃣
'@id': kid, answer: answerInput.value
});
});
knowledgeBaseDiv.insertAdjacentElement('beforeend', knowledgeDiv);
});
}
<template class="help">
<p>This example shows how an app can establish some user interface state from a <b>m-ld</b> domain, and then follow updates to the domain; in this case, just refreshing the UI every time. Changes arising in the app are pushed to the domain, where they are 'echoed' back as updates so that the UI presents the local user's changes, as well as any remote users, in the same way.</p>
<p>The code demonstrates:</p>
<ul>
<li>1️⃣ Using a <a href="https://js.m-ld.org/interfaces/meldstatemachine.html#read">read procedure</a> with a follow handler.</li>
<li>2️⃣ Using a <a href="https://js.m-ld.org/interfaces/meldreadstate.html#read">read request</a> to issue a structured query.</li>
<li>3️⃣ Using a <a href="https://js.m-ld.org/interfaces/describe.html"><code>@describe</code> query</a> to obtain all the properties of all subjects in the domain.</li>
<li>4️⃣ Using <code>each()</code> on the <a href="https://js.m-ld.org/interfaces/readresult.html">read result</a> to process each subject in turn.</li>
<li>5️⃣ Using a <a href="https://js.m-ld.org/interfaces/meldstatemachine.html#write">write request</a> to add new information to the domain.</li>
</ul>
</template>
import { array } from 'https://js.m-ld.org/ext/index.mjs';
document.addEventListener('domainChanged', async () => {
window.model.state.read(
state => {
// Populate the UI from scratch when the domain has changed.
knowledgeBaseDiv.innerHTML = '';
return state.read({
'@describe': '?id', '@where': { '@id': '?id' }
}).each(addQuestion);
},
update => {
// Our app's updates are always either one question or one answer
const { '@insert': [knowledge] } = update; // 1️⃣
const { '@id': kid, answer } = knowledge;
if (answer) // A new answer
addAnswer(document.querySelector(`#${kid} .answerList`), answer);
else // A new knowledge item
addQuestion(knowledge);
}
);
});
function addAnswer(answerList, answer) {
answerList.insertAdjacentHTML('beforeend', `<li>${answer}</li>`);
}
function addQuestion({ '@id': kid, question, answer }) {
const {
knowledgeDiv, questionText, answerList, answerInput, addAnswerButton
} = templated(knowledgeTemplate);
questionText.innerText = question;
// The `answer` field can be one string or an array
for (let ans of array(answer))
addAnswer(answerList, ans);
addAnswerButton.addEventListener('click', () => {
answerInput.value && window.model.state.write({
'@id': kid, answer: answerInput.value
});
});
// Set the element id so we can find it later
knowledgeDiv.id = kid;
knowledgeBaseDiv.insertAdjacentElement('beforeend', knowledgeDiv);
}
<template class="help">
<p>Instead of just reloading the whole UI state when an update comes in (as in the previous example), it might be better to inspect the update to see what has changed, and act upon only the changed information. This actually fixes a bug you might have noticed with the reloading example: an answer box can get replaced while you're typing into it, if someone else makes a change.</p>
<p>The code demonstrates:</p>
<ul>
<li>1️⃣ Inspecting the contents of a <a href="https://js.m-ld.org/interfaces/meldupdate.html"><b>m-ld</b> update</a> to target specific app UI components.</li>
</ul>
</template>
import { array, SubjectUpdater } from 'https://js.m-ld.org/ext/index.mjs';
const knowledgeViews = new Map();
document.addEventListener('domainChanged', async () => {
window.model.state.read(
async state => {
// Populate the UI from scratch when the domain has changed.
knowledgeBaseDiv.innerHTML = '';
const subjectUpdater = new SubjectUpdater(await state.read({ // 1️⃣
'@describe': '?id', '@where': { '@id': '?id' }
}));
for (let kid of subjectUpdater.affectedIds)
subjectUpdater.update(new KnowledgeView(kid));
},
update => {
const subjectUpdater = new SubjectUpdater(update); // 2️⃣
for (let kid of subjectUpdater.affectedIds)
subjectUpdater.update(knowledgeViews.get(kid) ?? new KnowledgeView(kid));
}
);
});
class KnowledgeView {
constructor(kid) {
this['@id'] = kid;
const {
knowledgeDiv, questionText, answerList, answerInput, addAnswerButton
} = templated(knowledgeTemplate);
this.questionText = questionText;
this.answerList = answerList;
addAnswerButton.addEventListener('click', () => {
answerInput.value && window.model.state.write({
'@id': kid, answer: answerInput.value
});
});
knowledgeViews.set(kid, this);
knowledgeBaseDiv.insertAdjacentElement('beforeend', knowledgeDiv);
}
get question() {
return this.questionText.innerText || [];
}
set question(question) {
this.questionText.innerText = question;
}
get answer() {
return [...this.answerList.children].map(li => li.innerText);
}
set answer(answer) {
this.answerList.innerHTML = '';
// The `answer` field can be one string or an array
for (let ans of array(answer))
this.answerList.insertAdjacentHTML('beforeend', `<li>${ans}</li>`);
}
}
<template class="help">
<p>As your app's features and data model scale, it becomes more complex to deconstruct updates and apply them to selected UI elements, as in the previous example. In this code, we declaratively model the knowledge UI as a class having question and answer properties. Then, we use a <a href="https://js.m-ld.org/classes/subjectupdater.html"><code>SubjectUpdater</code></a> to apply new information. This utility correctly handles all the possible changes that might apply in an update, taking into account <a href="https://spec.m-ld.org/#data-semantics"><b>m-ld</b> data semantics</a> – so, for example, it will still work when we start to allow deletion of answers, or if we include multiple questions in a single update.</p>
<p>The code demonstrates:</p>
<ul>
<li>1️⃣ Constructing a <code>SubjectUpdater</code> with newly-loaded subjects, and then using it to apply the loaded property values to new knowledge views.</li>
<li>2️⃣ Constructing a <code>SubjectUpdater</code> from an update, and then using it to apply the changed property values to knowledge views (which may be new, or already exist).</li>
</ul>
</template>
One of the most common questions asked about live information models is, what happens if there is a "conflict"? Here, we handle one particular kind of conflict using declarative constraints.
import { updateSubject } from 'https://js.m-ld.org/ext/index.mjs';
import { ShapeConstrained, PropertyShape } from 'https://js.m-ld.org/ext/shacl.mjs';
document.addEventListener('domainChanged', async () => {
if (window.model.genesis && false) { // 1️⃣
await window.model.state.write(
ShapeConstrained.declare(0, PropertyShape.declare({
path: 'name', count: 1
}))
);
}
const author = {
'@id': 'author',
// Naive UI ↔︎ data mapping, don't do this! 2️⃣
set name(name) { nameInput.value = name; },
get name() { return nameInput.value.split(',').filter(v => v); }
};
await window.model.state.read(
async state => updateSubject(author, await state.get('author')),
update => updateSubject(author, update)
);
beginEditSession();
});
function beginEditSession() {
window.model.state.write(state => new Promise(release => { // 3️⃣
const oldName = nameInput.value;
nameInput.readOnly = false;
nameInput.focus();
editButton.innerText = 'Enter';
editButton.addEventListener('click', async function enter() {
if (nameInput.value)
await state.write({ '@update': { '@id': 'author', name: nameInput.value } });
else
nameInput.value = oldName; // Revert
nameInput.readOnly = true;
editButton.innerText = 'Edit';
editButton.removeEventListener('click', enter);
release();
});
}));
}
editButton.addEventListener('click', () => {
if (nameInput.readOnly)
beginEditSession();
});
<div id="appDiv" hidden>
<h2>Author</h2>
<label for="nameInput">Name:</label>
<input id="nameInput" type="text" readonly/>
<button id="editButton">Edit</button>
</div>
<template class="help">
<p>
This example shows how "conflicts" can arise in user sessions, and one way to change the
default behaviour of <b>m-ld</b>, using <i>Shapes</i>.
</p>
<p>
In our app, we intend that the "author" subject should have only one "name" property value. Our
user interface presents the name, and allows us to change it using the Edit button. However, if
another user simultaneously changes the name, it's possible for the author to end up with
<i>both</i> entered names. (Try it by following the instructions above to duplicate this tab,
and beginning an edit in both tabs.)
</p>
<ul>
<li>
1️⃣ Here we declare that the "name" property should have only one value. When you have
explored the behaviour without this fix, change <code>false</code> to <code>true</code>
in this line, and try again with a new domain.
</li>
<li>
2️⃣ Here we are relying on the behaviour of an HTML text input element – if you set its value
to an array, it will separate the array values with a comma. This won't work as expected if
the name you enter has a comma in it, so a more robust approach would be needed in a real app.
</li>
<li>
3️⃣ Using a <a href="https://js.m-ld.org/interfaces/meldstatemachine.html#write">"state procedure"</a>
allows us to prevent <b>m-ld</b> from accepting remote updates until the returned promise
settles. This means that we don't see the effect of a concurrent edit until our editing
"session" is finished.
</li>
</ul>
</template>
#nameInput[readonly] {
border: none;
}
By default, information in m-ld is an unordered graph (just like in a relational database). This example shows how ordered lists can be embedded in the graph.
import { updateSubject } from 'https://js.m-ld.org/ext/index.mjs';
document.addEventListener('domainChanged', async () => {
shoppingList.innerHTML = '';
const { state, genesis } = window.model;
if (genesis) {
// Write some initial shopping items to the state
state.write({
'@id': 'shopping',
'@list': ['bread', 'milk']
});
}
// To use updateSubject for updating the DOM, we use a Javascript object-like
// proxy pattern over the relevant Elements.
const shopping = {
'@id': 'shopping',
'@list': {
// For a List, updateSubject can apply the update to anything with a
// `length` and an Array-like `splice` method.
get length() {
return shoppingList.childElementCount;
},
splice(index, deleteCount, ...items) {
for (let i = 0; i < deleteCount; i++)
shoppingList.children[index]?.remove();
const { el, position } = index < this.length ?
{ el: shoppingList.children[index], position: 'beforebegin' } :
{ el: shoppingList, position: 'beforeend' };
for (let item of items)
el.insertAdjacentHTML(position, `<li>${item}</li>`);
}
}
};
state.read(
async state => updateSubject(shopping, await state.get('shopping')),
update => updateSubject(shopping, update)
);
});
addItem.addEventListener('click', () => {
window.model.state.write({
'@id': 'shopping',
// When writing list items, we can use an object with integer keys instead
// of an array. Here we're inserting at the end of the list.
'@list': { [shoppingList.childElementCount]: itemToAdd.value }
});
});
removeItem.addEventListener('click', () => {
window.model.state.write({
'@delete': {
'@id': 'shopping',
// When deleting list items, we can pattern-match using variables. Here,
// we want to delete the removed item wherever it appears in the list.
'@list': { '?': itemToRemove.value }
}
});
});
<div id="appDiv" hidden>
<h2>Shopping</h2>
<ol id="shoppingList"></ol>
<p>
<input id="itemToAdd" type="text" placeholder="new shopping item"/>
<button id="addItem">+ Add</button>
</p>
<p>
<input id="itemToRemove" type="text" placeholder="shopping item to remove"/>
<button id="removeItem">- Remove</button>
</p>
<hr/>
</div>
Text embedded in a structured graph of information might need to be editable by multiple users at the same time, like an online document. This example shows the use of an embedded data type and a supporting HTML control, to enable multi-player text editing.
import { TSeqText } from 'https://js.m-ld.org/ext/tseq.mjs';
import { updateSubject } from 'https://js.m-ld.org/ext/index.mjs';
import { ElementSpliceText } from 'https://js.m-ld.org/ext/html.mjs';
document.addEventListener('domainChanged', () => {
if (window.model.genesis) {
window.model.state.write(TSeqText.declare(0, 'text'))
// Write some initial document content
.then(() => window.model.state.write({
'@id': 'document',
'text': `Document created ${new Date().toLocaleString()}`
}));
}
let documentTextProxy = null;
const doc = {
'@id': 'document',
set text(initialText) {
documentTextProxy = new ElementSpliceText(
documentTextDiv,
window.model.state,
'document',
'text',
initialText
);
},
get text() {
return documentTextProxy;
}
};
window.model.state.read(
async state => updateSubject(doc, await state.get('document')),
update => updateSubject(doc, update)
);
});
<div id="appDiv" hidden>
<h2>Document</h2>
<div contenteditable="plaintext-only" id="documentTextDiv"></div>
</div>
div[contenteditable] {
border: 1px inset #ccc;
padding: 5px;
background-color: white;
font-family: monospace;
height: 20em;
}
The information in m-ld is stored using the W3C standard data representation RDF (Resource Description Framework). For RDF-native apps, the Javascript engine API supports direct access to the RDF graph.
document.addEventListener('domainChanged', async () => {
updatesDiv.innerHTML = '';
for await (let [update] of window.model.state.follow()) {
const { details, summary, deleteTextarea, insertTextarea } =
templated(updateTemplate);
summary.innerText = `Update ${update['@ticks']}`;
const deleted = update['@delete'].quads;
const inserted = update['@insert'].quads;
deleteTextarea.value = await toTurtle(deleted);
insertTextarea.value = await toTurtle(inserted);
updatesDiv.insertAdjacentElement('afterbegin', details);
}
});
updateButton.addEventListener('click', async () => {
const parser = new N3.Parser();
window.model.state.updateQuads({
delete: await parser.parse(deleteTextarea.value),
insert: await parser.parse(insertTextarea.value)
});
});
function toTurtle(quads) {
return new Promise((resolve, reject) => {
const writer = new N3.Writer();
for (let quad of quads)
writer.addQuad(quad);
writer.end((err, result) => err ? reject(err) : resolve(result));
});
}
<div id="appDiv" hidden>
<label for="deleteTextarea">DELETE triples</label>
<textarea id="deleteTextarea" rows="5"></textarea>
<label for="insertTextarea">INSERT triples</label>
<textarea id="insertTextarea" rows="5">
PREFIX c: <http://example.org/cartoons#>
c:Tom a c:Cat.
c:Jerry a c:Mouse;
c:smarterThan c:Tom.
</textarea>
<button id="updateButton">
Do Update
</button>
<hr/>
<div id="updatesDiv"></div>
<template id="updateTemplate">
<details class="updateDetails">
<summary>Update</summary>
<hr/>
<label>DELETED triples</label>
<textarea class="deleteTextarea" rows="5"></textarea>
<label>INSERTED triples</label>
<textarea class="insertTextarea" rows="5"></textarea>
</details>
</template>
</div>
<!-- https://www.npmjs.com/package/n3 -->
<script src="https://unpkg.com/n3/browser/n3.min.js"></script>
textarea {
width: 100%;
}
.updateDetails {
border: 1px solid #aaa;
border-radius: 4px;
padding: 0.5em;
}
To mitigate integrity, confidentiality and availability threats to m-ld domain data, we recommend the following baseline security controls for your app.
A more general discussion of security considerations in m-ld can be found on the website.
🧪 This library additionally includes an experimental extension for controlling access based on an Access Control List (ACL) in the domain data. Please see our Security Project for more details of our ongoing security research, and contact us to discuss your security requirements!
m-ld supports extensions to its core engine. You can choose which extensions to use in an app; some are bundled in the engine package, others you can write yourself.
Some extensions must be pre-selected by the app in order to connect a new clone to a domain of information. Other extensions can be declared in the data and loaded dynamically by the engine at runtime. This allows authors to add features to the shared information, without the need for a central authority over features. This is decentralised extensibility, similar to how a web page can declare scripts that extend its features in the browser. See our introductory short paper for more about this vision.
The Javascript engine package bundles the following extensions – follow the links to learn more:
The extension's code module must be available to a global CommonJS-style require
method in all clones using the Javascript engine. For bundled extensions:
@m-ld/m-ld
; no additional configuration is required.require
is typically provided by the bundler. Since the module will be loaded dynamically, the bundler may need to be configured to guarantee the module is bundled, since it may not be referenced statically by any code.💡 While it's possible to change extensions at runtime (by changing their declarations in the data), this may require coordination between clones, to prevent outdated clones from acting incorrectly in ways that could cause data corruption or compromise security. Consult the extension's documentation for safe operation.
Extension code is executed as required by the core engine or by another extension. Besides remotes, there are currently four types of custom extension called by the core engine, defined in the MeldExtensions API interface. To write an extension to the core, you must implement one or more of these types.
💡 Please do contact us if you would like to understand more about extensions.
Initial definition of a m-ld app. Extensions provided will be used for bootstrapping, prior to the clone joining the domain. After that, different extensions may come into effect if so declared in the data.
A fully-identified Subject from the backend.
A function type to find the correct Datatype for an identifier and optionally a property in the domain.
If property
is provided, datatype
is the datatype of a literal at the
given property position in a Subject. Otherwise, it is the identity of the
datatype itself (which may be the same).
Delete-insert of quads, augmented with m-ld-specific details.
A subscription to a state machine. Can be unsubscribed to stop receiving updates. The subscription itself can also be async-iterated. Finally, the subscription may have a resolved value that can be awaited.
When used as an async iterable, it's important to begin iteration synchronously in order not to miss any updates. It is safe to await the subscription resolved value, if applicable – but it's rare to need both the resolved value and iteration.
When used as a promise, calling unsubscribe before the promise is settled may
cause it to reject with EmptyError
.
Property cardinality specification
Convenience specification for a PropertyShape
A function type specifying a 'procedure' during which a clone state is available as immutable. Strictly, the immutable state is guaranteed to remain 'live' until the procedure's return Promise resolves or rejects.
can be MeldReadState (default) or MeldState. If the latter, the state can be transitioned to another immutable state using MeldState.write.
An update form that mirrors the structure of a GraphUpdate, having optional keys
A function type specifying a 'procedure' during which a clone state is available as immutable following an update. Strictly, the immutable state is guaranteed to remain 'live' until the procedure's return Promise resolves or rejects.
A basic atomic value used as a concrete value or in a filter. Note that the
m-ld Javascript engine natively supports Uint8Array
for binary data.
An operator-based constraint of the form { <operator> : [<expression>...] }
. The key is the operator, and the value is the array of arguments. If the
operator is unary, the expression need not be wrapped in an array.
Used to express an ordered or unordered container of data.
A JSON-LD context for some JSON content such as a Subject. m-ld does not require the use of a context, as plain JSON data will be stored in the context of the domain. However in advanced usage, such as for integration with existing systems, it may be useful to provide other context for shared data.
An JSON-LD expanded term definition, as part of a domain Context.
A stand-in for a Value used as a basis for filtering.
A reference to a Subject. Used to disambiguate an IRI from a plain string. Unless a custom Context is used for the clone, all references will use this format.
This type is also used to distinguish identified subjects (with an @id
field) from anonymous ones (without an @id
field).
Result declaration of a Select query.
Use of '*'
specifies that all variables in the query should be returned.
'Properties' of a Subject, including from List and Slot.
Strictly, these are possible paths to a SubjectPropertyObject
aggregated by the Subject. An @list
contains numeric indexes (which may be
numeric strings or variables). The second optional index is used for multiple
items being inserted at the first index, using an array.
The allowable types for a Subject property value, named awkwardly to avoid
overloading Object
. Represents the "object" of a property, in the sense of
the object of discourse.
The expected type of the parameters to the @splice
operator.
A query variable, prefixed with "?", used as a placeholder for some value in a query, for example:
{
"@select": "?name",
"@where": { "employeeNo": 7, "name": "?name" }
}
Like a Reference, but used for "vocabulary" references. These are relevant to:
@type
: the type value is a vocabulary reference@vocab
in the ContextA query pattern that writes data to the domain. A write can be:
Note that this type does not fully capture the details above. Use isWrite to inspect a candidate pattern.
A Javascript atom value type constructor, one of:
String
Number
Boolean
Date
Uint8Array
Subject
Reference
VocabReference
A Javascript container value type constructor, one of:
Array
Set
Optional
Javascript atom constructors for types that can be obtained from graph subject properties.
Javascript container constructors for types that can be obtained from graph subject properties.
Javascript constructors for types that can be obtained from graph subject properties.
A Javascript value type constructor
Subject utilities are generally tolerant of Javascript objects that are not quite Subjects; for example, they may be various kinds of proxies.
An update to a single graph Subject.
Things that can be interpreted as an update to graph subjects
A m-ld update notification, indexed by graph Subject ID.
Simplified form of GraphUpdate, with plain Subject arrays
A read-only RDF dataset-like collection of Quads.
Implicit supertype of Algebra.DeleteInsert that does not require a factory
Abstract stream of any type; implicit supertype of an RDF/JS Stream
Standardised constructor type for ORM subjects
Operation with optional revert metadata – the revert component is required if the local operation is to be reverted.
An operation against the TSeq data types comprises a set of runs.
A revert of a TSeq operation encodes the prior state of each char-tick in an
operation. It has the same length as its corresponding operation, where each
index in the outer array matches an operation run. An undefined
entry
indicates that no change happened for that char-tick.
A 'run' is a sequence of affected characters (content) at an anchor position in the tree identified by a path, which is an array of names.
Constructor of references from references: used similarly to e.g. Number
Constructor of subjects from subjects: used similarly to e.g. Number
Constructor of vocab references from vocab references: used similarly to e.g. Number
Symbolic object for missing Javascript Optional monad
Combines plugins. The extensions are dynamically iterated, so the passed
Iterable
can change content after this function is called.
the extensions to combine
an object containing any additional properties to include
Create or initialise a local clone, depending on whether the given backend already contains m-ld data. This function returns as soon as it is safe to begin transactions against the clone; this may be before the clone has received all updates from the domain. You can wait until the clone is up-to-date using the MeldClone.status property.
an instance of a leveldb backend
remotes constructor
the clone configuration
Determines whether the given property object from a well-formed Subject is a
graph edge; i.e. not a @context
or the Subject @id
.
the Subject property in question
the object (value) of the property
Determines if the given pattern will read data from the domain.
Determines if the given pattern can probably be interpreted as a logical write of data to the domain.
This function is not exhaustive, and a pattern identified as a write can
still turn out to be illogical, for example if it contains an @insert
with
embedded variables and no @where
clause to bind them.
Returns true
if the logical write is a trivial no-op, such as {}
,
{ "@insert": {} }
or { "@graph": [] }
.
A utility to generate a variable with a unique Id. Convenient to use when generating query patterns in code.
Utility to normalise a property value according to m-ld
data semantics, from a missing
value (null
or undefined
), a single value, or an array of values, to an
array of values (empty for missing values). This can simplify processing of
property values in common cases.
the value to normalise to an array
Provides an alternate view of the update deletes and inserts, by Subject.
An update is presented with arrays of inserted and deleted subjects:
{
"@delete": [{ "@id": "foo", "severity": 3 }],
"@insert": [
{ "@id": "foo", "severity": 5 },
{ "@id": "bar", "severity": 1 }
]
}
In many cases it is preferable to apply inserted and deleted properties to app data views on a subject-by-subject basis. This property views the above as:
{
"foo": {
"@delete": { "@id": "foo", "severity": 3 },
"@insert": { "@id": "foo", "severity": 5 }
},
"bar": {
"@delete": {},
"@insert": { "@id": "bar", "severity": 1 }
}
}
Javascript references to other Subjects in a Subject's properties will always
be collapsed to json-rql Reference objects (e.g. { '@id': '<iri>' }
).
the update to convert
if flagged, each subject in the update is cloned
A utility to generate a unique blank node.
Top-level utility version of JsType.cast
Includes the given value in the Subject property, respecting m-ld data semantics by expanding the property to an array, if necessary.
the subject to add the value to.
the property that relates the value to the subject.
the value to add.
the subject to inspect
the property to inspect
the value or values to find in the set. If undefined
, then
wildcard checks for unknown value at all. If an empty array, always returns true
An atom value merge strategy that takes the maximum value. Subjects,
References and VocabReferences are compared by their identity (@id
or
@vocab
).
the atom type
the values to merge by finding the maximum
An atom value merge strategy that refused to merge and throws. This should be used in situations where a exception is suitable for the application logic.
Note that in many situations it may be better to declare the property as an
Array
or Set
, and to present the conflict to the user for resolution.
the atom type
the values to merge by throwing an exception
Reverse of castPropertyValue: normalises a JavaScript value to a JSON-LD value suitable for use in a Subject.
Top-level utility version of JsProperty.value
Utility to generate a short Id according to the given spec.
If provided, a stable obfuscated Id will be generated for the string with a fast hash.
a string identifier that is safe to use as an HTML (& XML) element Id
Generates the difference between the texts in the form of a splice suitable
for use with the @splice
operator.
Applies an update to the given subject in-place. This method will correctly apply the deleted and inserted properties from the update, accounting for m-ld data semantics.
Referenced Subjects will also be updated if they have been affected by the
given update, deeply. If a reference property has changed to a different
object (whether or not that object is present in the update), it will be
updated to a json-rql Reference (e.g. { '@id': '<iri>' }
).
Changes are applied to non-@list
properties using only L-value assignment,
so the given Subject can be safely implemented with property setters, such as
using set
in a class, or by using defineProperty
, or using a Proxy; for
example to trigger side-effect behaviour. Removed properties are set to an
empty array ([]
) to signal emptiness, and then deleted.
Changes to @list
items are enacted in reverse index order, by calling
splice
. If the @list
value is a hash, it will have a length
property
added. To intercept these calls, re-implement splice
on the @list
property value.
Changes to text represented as a shared datatype
supporting the @splice
operator will be correctly applied to plain strings,
and can also be applied to any Object having a splice
method taking three
parameters, index
, deleteCount
and content
. This allows an app to
efficiently apply character-level changes, e.g. to document editing
components. The proxy object should also implement a toJSON()
method which
returns the current string state.
CAUTION: If this function is called independently on subjects which reference each other via Javascript references, or share referenced subjects, then the referenced subjects may be updated more than once, with unexpected results. To avoid this, use a SubjectUpdater to process the whole update.
the app-specific subject type of interest
the resource to apply the update to
the update, as a MeldUpdate or obtained from asSubjectUpdates
if false
, any unsupported data expressions in the
update will cause a RangeError
– useful in development to catch problems
early
Utility to generate a unique short UUID for use in a MeldConfig; actually
a CUID starting with the character c
and containing only lowercase
US-English letters and digits. (Note that this is not an RFC 4122 UUID.)
Utility to determine if some run content includes any character inserts
Shorthand annotation to declare an ORM subject field to be mapped to a JSON-LD graph property (an edge).
the JSON-LD type of the graph subject property
the JSON-LD property. If undefined, the field name will be used
Generated using TypeDoc. Delivered by Vercel. @m-ld/m-ld - v0.10.0 Source code licensed MIT. Privacy policy
Configuration of the clone data constraint. The supported constraints are:
single-valued
: the given property should have only one value. The property can be given in unexpanded form, as it appears in JSON subjects when using the API, or as its full IRI reference.See Shape