diff --git a/examples/gelf/gelf.js b/examples/gelf/gelf.js
new file mode 100644
index 00000000..2fa8f9e4
--- /dev/null
+++ b/examples/gelf/gelf.js
@@ -0,0 +1,213 @@
+import '../../../gun/gun.js'
+const gun = window.Gun(location.origin + '/gun');
+
+const database = {}
+const logs = {}
+
+export function insights() {
+ return logs
+}
+
+function insight(name, link) {
+ if(!logs[`${name}:${link}`]) {
+ logs[`${name}:${link}`] = 0
+ }
+ logs[`${name}:${link}`] += 1
+}
+
+const CREATE_EVENT = 'create'
+
+const observableEvents = [CREATE_EVENT]
+
+function update(link, target, compositor, lifeCycle={}) {
+ insight('module:update', link)
+ if(lifeCycle.beforeUpdate) {
+ lifeCycle.beforeUpdate(target)
+ }
+
+ const html = compositor(target)
+ if(html) target.innerHTML = html
+
+ if(lifeCycle.afterUpdate) {
+ lifeCycle.afterUpdate(target)
+ }
+}
+
+function draw(link, compositor, lifeCycle={}) {
+ insight('module:draw', link)
+ listen(CREATE_EVENT, link, (event) => {
+ gun.get(this.seed).get(link).on(cache => {
+ database[link] = JSON.parse(cache) || {}
+ update(link, event.target, compositor, lifeCycle)
+ })
+ })
+}
+
+function style(link, stylesheet) {
+ insight('module:style', link)
+ const styles = `
+
+ `;
+
+ document.body.insertAdjacentHTML("beforeend", styles)
+}
+
+export function learn(link) {
+ insight('module:learn', link)
+ return database[link] || {}
+}
+
+export function teach(link, knowledge, nuance = (s, p) => ({...s,...p})) {
+ insight('module:teach', link)
+ gun.get(this.seed).get(link).once(cache => {
+ const data = cache ? JSON.parse(cache) : {}
+ const latest = nuance(data, knowledge);
+ gun.get(this.seed).get(link).put(JSON.stringify(latest))
+ })
+}
+
+export function when(link1, type, link2, callback) {
+ const link = `${link1} ${link2}`
+ insight('module:when:'+type, link)
+ listen(type, link, callback)
+}
+
+export default function module(link, initialState = {}) {
+ insight('module', link)
+ teach.call(this, link, initialState)
+
+ return {
+ link,
+ learn: learn.bind(this, link),
+ draw: draw.bind(this, link),
+ style: style.bind(this, link),
+ when: when.bind(this, link),
+ teach: teach.bind(this, link),
+ }
+}
+
+export function subscribe(fun) {
+ notifications[fun.toString] = fun
+}
+
+export function unsubscribe(fun) {
+ if(notifications[fun.toString]) {
+ delete notifications[fun.toString]
+ }
+}
+
+export function listen(type, link, handler = () => null) {
+ const callback = (event) => {
+ if(
+ event.target &&
+ event.target.matches &&
+ event.target.matches(link)
+ ) {
+
+ insight('module:listen:'+type, link)
+ handler.call(null, event);
+ }
+ };
+
+ document.addEventListener(type, callback, true);
+
+ if(observableEvents.includes(type)) {
+ observe(link);
+ }
+
+ return function unlisten() {
+ if(type === CREATE_EVENT) {
+ disregard(link);
+ }
+
+ document.removeEventListener(type, callback, true);
+ }
+}
+
+let links = []
+
+function observe(link) {
+ links = [...new Set([...links, link])];
+ maybeCreateReactive([...document.querySelectorAll(link)])
+}
+
+function disregard(link) {
+ const index = links.indexOf(link);
+ if(index >= 0) {
+ links = [
+ ...links.slice(0, index),
+ ...links.slice(index + 1)
+ ];
+ }
+}
+
+function maybeCreateReactive(targets) {
+ targets
+ .filter(x => !x.reactive)
+ .forEach(dispatchCreate)
+}
+
+function getSubscribers({ target }) {
+ if(links.length > 0)
+ return [...target.querySelectorAll(links.join(', '))];
+ else
+ return []
+}
+
+function dispatchCreate(target) {
+ insight('module:create', target.localName)
+ if(!target.id) target.id = sufficientlyUniqueId()
+ target.dispatchEvent(new Event(CREATE_EVENT))
+ target.reactive = true
+}
+
+new MutationObserver((mutationsList) => {
+ const targets = [...mutationsList]
+ .map(getSubscribers)
+ .flatMap(x => x)
+ maybeCreateReactive(targets)
+}).observe(document.body, { childList: true, subtree: true });
+
+function sufficientlyUniqueId() {
+ // https://stackoverflow.com/a/2117523
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+ const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
+ return v.toString(16);
+ });
+}
+
+tags({ registry: '/examples/gelf/tags' })
+new MutationObserver(() => {
+ tags({ registry: '/examples/gelf/tags' })
+}).observe(document.body, { childList: true, subtree: true });
+function tags({ registry }) {
+ const tags = new Set(
+ [...document.querySelectorAll(':not(:defined)')]
+ .map(({ tagName }) => tagName.toLowerCase())
+ )
+
+ tags.forEach(async (tag) => {
+ const url = `${registry || '.'}/${tag}.js`
+ const exists = (await fetch(url, { method: 'HEAD' })).ok
+ if(!exists) return
+ let definable = true
+ await import(url).catch((e) => {
+ definable = false
+ console.error(e)
+ })
+ try {
+ definable = definable && document.querySelector(tag) && document.querySelector(tag).matches(':not(:defined)')
+ if(definable) {
+ customElements.define(tag, class WebComponent extends HTMLElement {
+ constructor() {
+ super();
+ }
+ });
+ }
+ } catch(e) {
+ console.log('Error defining module:', tag, e)
+ }
+ })
+}
diff --git a/examples/gelf/hello-world.html b/examples/gelf/hello-world.html
new file mode 100644
index 00000000..b56f0387
--- /dev/null
+++ b/examples/gelf/hello-world.html
@@ -0,0 +1,2 @@
+
+
diff --git a/examples/gelf/note.html b/examples/gelf/note.html
new file mode 100644
index 00000000..69435770
--- /dev/null
+++ b/examples/gelf/note.html
@@ -0,0 +1,27 @@
+
+
+
+
diff --git a/examples/gelf/tags/hello-world.js b/examples/gelf/tags/hello-world.js
new file mode 100644
index 00000000..8fedf715
--- /dev/null
+++ b/examples/gelf/tags/hello-world.js
@@ -0,0 +1,3 @@
+import gelf from '/examples/gelf/gelf.js'
+
+gelf('hello-world').draw(() => `Hello World`)