UNPKG

hybrids

Version:

A JavaScript framework for creating fully-featured web applications, components libraries, and single web components with unique declarative and functional architecture

231 lines (182 loc) 4.62 kB
import * as emitter from "./emitter.js"; const entries = new WeakMap(); const stack = new Set(); function dispatch(entry, resolved = false) { const contexts = []; let index = 0; entry.resolved = resolved; while (entry) { if (entry.contexts) { for (const context of entry.contexts) { if (!stack.has(context) && !contexts.includes(context)) { context.resolved = false; contexts.push(context); } } } if (entry.observe) { emitter.add(entry.observe); } entry = contexts[index++]; } } export function getEntry(target, key) { let map = entries.get(target); if (!map) { map = new Map(); entries.set(target, map); } let entry = map.get(key); if (!entry) { entry = { key, target, value: undefined, assertValue: undefined, lastValue: undefined, resolved: false, contexts: undefined, deps: undefined, observe: undefined, }; map.set(key, entry); } return entry; } export function getEntries(target) { const targetMap = entries.get(target); if (targetMap) return [...targetMap.values()]; return []; } let context = null; export function getCurrentValue() { return context?.value; } export function get(target, key, fn) { const entry = getEntry(target, key); if (context) { if (!entry.contexts) entry.contexts = new Set(); if (!context.deps) context.deps = new Set(); entry.contexts.add(context); context.deps.add(entry); } if (entry.resolved) return entry.value; if (entry.deps) { for (const depEntry of entry.deps) { depEntry.contexts.delete(entry); } entry.deps.clear(); } const lastContext = context; try { if (stack.has(entry)) { throw Error(`Circular get invocation is forbidden: '${key}'`); } context = entry; stack.add(entry); entry.value = fn(target, entry.assertValue); entry.resolved = true; context = lastContext; stack.delete(entry); } catch (e) { context = lastContext; stack.delete(entry); if (context) { context.deps.delete(entry); entry.contexts.delete(context); } throw e; } return entry.value; } export function assert(target, key, value, force) { if (context && context.target === target && !force) { throw Error( `Try to update the '${key}' property while getting the '${context.key}' property`, ); } const entry = getEntry(target, key); entry.value = undefined; entry.assertValue = value; dispatch(entry); } export function sync(target, key, fn, value) { const entry = getEntry(target, key); const nextValue = fn(target, value, entry.value); if (nextValue !== entry.value) { entry.value = nextValue; entry.assertValue = undefined; dispatch(entry, true); // mark as resolved to avoid double fn call in get entry.resolved = true; } } export function observe(target, key, fn, callback) { const entry = getEntry(target, key); entry.observe = () => { const value = get(target, key, fn); if (value !== entry.lastValue) { callback(target, value, entry.lastValue); entry.lastValue = value; } }; try { entry.observe(); } catch (e) { console.error(e); } return () => { entry.observe = undefined; entry.lastValue = undefined; }; } const gc = new Set(); function deleteEntry(entry) { if (!gc.size) { setTimeout(() => { for (const e of gc) { if (!e.contexts || e.contexts.size === 0) { const targetMap = entries.get(e.target); targetMap.delete(e.key); } } gc.clear(); }); } gc.add(entry); } function invalidateEntry(entry, options) { dispatch(entry); if (options.clearValue) { entry.value = undefined; entry.assertValue = undefined; entry.lastValue = undefined; } if (options.deleteEntry) { if (entry.deps) { for (const depEntry of entry.deps) { depEntry.contexts.delete(entry); } entry.deps = undefined; } if (entry.contexts) { for (const context of entry.contexts) { context.deps.delete(entry); } entry.contexts = undefined; } deleteEntry(entry); } } export function invalidate(target, key, options = {}) { const entry = getEntry(target, key); invalidateEntry(entry, options); } export function invalidateAll(target, options = {}) { const targetMap = entries.get(target); if (targetMap) { for (const entry of targetMap.values()) { invalidateEntry(entry, options); } } }