export const mark= Symbol.for("signal"); export function isSignal(candidate){ try{ return Reflect.has(candidate, mark); } catch(e){ return false; } } export function S(value, actions){ if(typeof value!=="function") return create(value, actions); if(isSignal(value)) return value; const out= create(); watch(()=> out(value())); return out; } S.action= function(signal, name, ...a){ if(!isSignal(signal)) throw new Error(`'${signal}' is not a signal!`); const s= signal[mark], { actions }= s; if(!actions || !Reflect.has(actions, name)) throw new Error(`'${signal}' has no action with name '${name}'!`); actions[name].apply(s, a); if(s.skip) return Reflect.deleteProperty(s, "skip"); s.listeners.forEach(l=> l(s.value)); }; S.on= function on(signals, listener, options){ if(Array.isArray(signals)) return signals.forEach(s=> on(s, listener, options)); addSignalListener(signals, listener); if(options && options.signal) options.signal.addEventListener("abort", ()=> removeSignalListener(signals, listener)); //TODO cleanup when signal removed (also TODO) } S.clear= function(...signals){ for(const signal of signals){ signal[mark].listeners.clear(); Reflect.deleteProperty(signal, mark); } }; import { typeOf } from './helpers.js'; export const signals_config= { isReactiveAtrribute(attr, key){ return isSignal(attr); }, isTextContent(attributes){ return typeOf(attributes)==="string" || ( isSignal(attributes) && typeOf(valueOfSignal(attributes))==="string" ); }, processReactiveAttribute(_, key, attrS, assignNth){ addSignalListener(attrS, attr=> assignNth([ key, attr ])); return attrS(); }, reactiveElement(signal, map){ const mark_start= document.createComment("<> #reactive"); const mark_end= document.createComment(" #reactive"); const out= document.createDocumentFragment(); out.append(mark_start, mark_end); const toEls= v=> { if(!mark_start.parentNode || !mark_end.parentNode) return removeSignalListener(signal, toEls); let els= map(v); if(!Array.isArray(els)) els= [ els ]; let el_r= mark_start; while(( el_r= mark_start.nextSibling ) !== mark_end) el_r.remove(); mark_start.after(...els); }; addSignalListener(signal, toEls); toEls(signal()); return out; } }; function create(value, actions){ const signal= (...value)=> value.length ? write(signal, value[0]) : read(signal); return toSignal(signal, value, actions); } const protoSigal= Object.assign(Object.create(null), { stopPropagation(){ this.skip= true; } }); function toSignal(signal, value, actions){ if(typeOf(actions)!=="[object Object]") actions= {}; signal[mark]= { value, actions, listeners: new Set(), }; Object.setPrototypeOf(signal[mark], protoSigal); return signal; } const stack_watch= []; export function watch(context){ const contextReWatch= function(){ stack_watch.push(contextReWatch); context(); stack_watch.pop(); }; stack_watch.push(contextReWatch); context(); stack_watch.pop(); }; function currentContext(){ return stack_watch[stack_watch.length - 1]; } function read(signal){ if(!signal[mark]) return; const { value, listeners }= signal[mark]; const context= currentContext(); if(context) listeners.add(context); return value; } function write(signal, value){ if(!signal[mark]) return; const s= signal[mark]; if(s.value===value) return; s.value= value; s.listeners.forEach(fn=> fn(value)) return value; } function valueOfSignal(signal){ return signal[mark].value; } function addSignalListener(signal, listener){ return signal[mark].listeners.add(listener); } function removeSignalListener(signal, listener){ return signal[mark].listeners.delete(listener); }