Options
All
  • Public
  • Public/Protected
  • All
Menu

This extension allows an app to declare that the domain data must conform to some defined shapes. A collection of shapes is like a 'schema' or 'object model'.

The extension can be declared in the data using declare, or instantiated and provided to the clone function in the app parameter, e.g.

api = await clone(
  new MemoryLevel, MqttRemotes, config,
  new ShapeConstrained(new PropertyShape({
    path: 'name', count: 1
  }))
);
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>
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;
}
see

https://www.w3.org/TR/shacl/

Hierarchy

  • ShapeConstrained

Implements

Index

Constructors

Properties

Methods

Constructors

constructor

  • Parameters

    • Rest ...shapes: Shape[]

      The shapes to which domain data must conform

    Returns ShapeConstrained

Properties

shapes

shapes: Shape[]

The shapes to which domain data must conform.

Methods

Static declare

  • Extension declaration. Insert into the domain data to install the extension. For example (assuming a m-ld clone object):

    clone.write(ShapeConstrained.declare(0, PropertyShape.declare({
      path: 'name', count: 1
    })));
    

    Note that declaration of shapes will not retrospectively apply constraints to any existing subjects in the domain. It's the app's responsibility to correct existing data, if necessary.

    Parameters

    • priority: number

      the preferred index into the existing list of extensions (lower value is higher priority).

    • Rest ...controlledShapes: (Subject | Reference)[]

      shape Subjects, or References to pre-existing shapes

    Returns Subject

Legend

  • Constructor
  • Property
  • Method
  • Property
  • Method
  • Inherited property
  • Inherited method
  • Static property
  • Static method
  • Protected method

Generated using TypeDoc. Delivered by Vercel. @m-ld/m-ld - v0.10.0 Source code licensed MIT. Privacy policy