UNPKG

hybrids

Version:

The simplest way to create web components with plain objects and pure functions!

259 lines (204 loc) 5.2 kB
import * as emitter from "./emitter.js"; const entries = new WeakMap(); const suspense = new WeakSet(); export function getEntry(target, key) { let targetMap = entries.get(target); if (!targetMap) { targetMap = new Map(); entries.set(target, targetMap); } let entry = targetMap.get(key); if (!entry) { entry = { target, key, value: undefined, lastValue: undefined, contexts: new Set(), deps: new Set(), state: 0, depState: 0, resolved: false, }; targetMap.set(key, entry); } return entry; } export function getEntries(target) { const result = []; const targetMap = entries.get(target); if (targetMap) { targetMap.forEach(entry => { result.push(entry); }); } return result; } function cleanContexts(entry) { entry.contexts.forEach(contextEntry => { if (suspense.has(contextEntry.target)) { Object.assign(contextEntry, { value: undefined, lastValue: undefined, depState: 0, resolved: false, }); entry.contexts.delete(contextEntry); cleanContexts(contextEntry); } }); } function dispatchDeep(entry) { entry.resolved = false; if (!suspense.has(entry.target)) { emitter.dispatch(entry); } cleanContexts(entry); entry.contexts.forEach(dispatchDeep); } let context = null; const contexts = new Set(); export function get(target, key, getter) { const entry = getEntry(target, key); if (context && !suspense.has(context.target)) { context.deps.add(entry); entry.contexts.add(context); } if (!suspense.has(target)) { cleanContexts(entry); if (entry.resolved) { return entry.value; } if (entry.depState > entry.state) { let depState = entry.state; for (const depEntry of entry.deps) { // eslint-disable-next-line no-unused-expressions depEntry.target[depEntry.key]; if (!depEntry.resolved) { depState = false; break; } depState += depEntry.state; } if (depState && depState === entry.depState) { entry.resolved = true; return entry.value; } } } const lastContext = context; try { if (contexts.has(entry)) { throw Error(`Circular get invocation is forbidden: '${key}'`); } entry.deps.forEach(depEntry => { depEntry.contexts.delete(entry); }); entry.deps.clear(); context = entry; contexts.add(entry); const nextValue = getter(target, entry.value); context = lastContext; if (nextValue !== entry.value) { entry.value = nextValue; entry.state += 1; } let depState = entry.state; entry.deps.forEach(depEntry => { depState += depEntry.state; }); entry.depState = depState; entry.resolved = !suspense.has(target); contexts.delete(entry); } catch (e) { context = lastContext; contexts.delete(entry); entry.resolved = false; if (context && !suspense.has(context)) { context.deps.delete(entry); entry.contexts.delete(context); } throw e; } return entry.value; } export function set(target, key, setter, value) { const entry = getEntry(target, key); const newValue = setter(target, value, entry.value); if (newValue !== entry.value) { entry.value = newValue; entry.state += 1; entry.depState = 0; dispatchDeep(entry); } } const gcList = new Set(); function deleteEntry(entry) { if (!gcList.size) { requestAnimationFrame(() => { gcList.forEach(e => { if (e.contexts.size === 0) { e.deps.forEach(depEntry => { depEntry.contexts.delete(e); }); const targetMap = entries.get(e.target); targetMap.delete(e.key); } }); gcList.clear(); }); } gcList.add(entry); } function invalidateEntry(entry, options) { entry.depState = 0; dispatchDeep(entry); if (options.clearValue) { entry.value = undefined; entry.lastValue = undefined; } if (options.deleteEntry) { deleteEntry(entry); } if (options.force) { entry.state += 1; } } export function invalidate(target, key, options = {}) { if (contexts.size) { throw Error( `Invalidating property in chain of get calls is forbidden: '${key}'`, ); } const entry = getEntry(target, key); invalidateEntry(entry, options); } export function invalidateAll(target, options = {}) { if (contexts.size) { throw Error( "Invalidating all properties in chain of get calls is forbidden", ); } const targetMap = entries.get(target); if (targetMap) { targetMap.forEach(entry => { invalidateEntry(entry, options); }); } } export function observe(target, key, getter, fn) { const entry = getEntry(target, key); return emitter.subscribe(entry, () => { const value = get(target, key, getter); if (value !== entry.lastValue) { fn(target, value, entry.lastValue); entry.lastValue = value; } }); } export function suspend(target) { suspense.add(target); } export function unsuspend(target) { suspense.delete(target); }