1
0
mirror of https://github.com/jaandrle/deka-dom-el synced 2025-07-01 12:22:15 +02:00

Replace “observable” term with “signal” (#19)

*  refact docs

to make editing (now renaming observables to signal) easier

*   use signal(s) term isntead of observable(s)

*  🔤 version + typo

* 🐛 customElement example (0→S)

* 📺 version in package-lock.json
This commit is contained in:
2024-05-22 21:43:49 +02:00
committed by GitHub
parent 4014e79740
commit cd62782c7b
65 changed files with 1426 additions and 978 deletions

View File

@ -1,4 +1,4 @@
import { observables } from "./observables-common.js";
import { signals } from "./signals-common.js";
import { enviroment as env } from './dom-common.js';
/** @type {{ scope: object, prevent: boolean, host: function }[]} */
@ -31,11 +31,11 @@ export function chainableAppend(el){ if(el.append===append) return el; el.append
let namespace;
export function createElement(tag, attributes, ...addons){
/* jshint maxcomplexity: 15 */
const s= observables(this);
const s= signals(this);
let scoped= 0;
let el, el_host;
//TODO Array.isArray(tag) ⇒ set key (cache els)
if(Object(attributes)!==attributes || s.isObservable(attributes))
if(Object(attributes)!==attributes || s.isSignal(attributes))
attributes= { textContent: attributes };
switch(true){
case typeof tag==="function": {
@ -177,11 +177,11 @@ function assignContext(element, _this){
if(assign_context.has(element)) return assign_context.get(element);
const is_svg= element instanceof env.S;
const setRemoveAttr= (is_svg ? setRemoveNS : setRemove).bind(null, element, "Attribute");
const s= observables(_this);
const s= signals(_this);
return { setRemoveAttr, s };
}
export function classListDeclarative(element, toggle){
const s= observables(this);
const s= signals(this);
forEachEntries(s, toggle,
(class_name, val)=>
element.classList.toggle(class_name, val===-1 ? undefined : Boolean(val)));

View File

@ -1,4 +1,4 @@
export { registerReactivity } from './observables-common.js';
export { registerReactivity } from './signals-common.js';
import { enviroment as env, keyLTE, evc, evd, eva } from './dom-common.js';
export function dispatchEvent(name, options, host){

View File

@ -1,13 +0,0 @@
export const observables_global= {
isObservable(attributes){ return false; },
processReactiveAttribute(obj, key, attr, set){ return attr; },
};
export function registerReactivity(def, global= true){
if(global) return Object.assign(observables_global, def);
Object.setPrototypeOf(def, observables_global);
return def;
}
/** @param {unknown} _this @returns {typeof observables_global} */
export function observables(_this){
return observables_global.isPrototypeOf(_this) && _this!==observables_global ? _this : observables_global;
}

View File

@ -1,4 +0,0 @@
import type { Action, Actions, observable as o, Observable, SymbolOnclear } from "../observables.d.ts";
export { Action, Actions, Observable, SymbolOnclear };
export const O: o;
export const observable: o;

View File

@ -1,26 +1,26 @@
export const mark= "__dde_observable";
export const mark= "__dde_signal";
import { hasOwn } from "./helpers.js";
export function isObservable(candidate){
export function isSignal(candidate){
try{ return hasOwn(candidate, mark); }
catch(e){ return false; }
}
/** @type {function[]} */
const stack_watch= [];
/**
* ### `WeakMap<function, Set<ddeObservable<any, any>>>`
* The `Set` is in the form of `[ source, ...depended observables (DSs) ]`.
* When the DS is cleaned (`O.clear`) it is removed from DSs,
* ### `WeakMap<function, Set<ddeSignal<any, any>>>`
* The `Set` is in the form of `[ source, ...depended signals (DSs) ]`.
* When the DS is cleaned (`S.clear`) it is removed from DSs,
* if remains only one (`source`) it is cleared too.
* ### `WeakMap<object, function>`
* This is used for revesed deps, the `function` is also key for `deps`.
* @type {WeakMap<function|object,Set<ddeObservable<any, any>>|function>}
* @type {WeakMap<function|object,Set<ddeSignal<any, any>>|function>}
* */
const deps= new WeakMap();
export function observable(value, actions){
export function signal(value, actions){
if(typeof value!=="function")
return create(false, value, actions);
if(isObservable(value)) return value;
if(isSignal(value)) return value;
const out= create(true);
const contextReWatch= function(){
@ -33,9 +33,9 @@ export function observable(value, actions){
if(!deps_old.length) return;
const deps_curr= deps.get(contextReWatch);
for (const dep_observable of deps_old){
if(deps_curr.has(dep_observable)) continue;
removeObservableListener(dep_observable, contextReWatch);
for (const dep_signal of deps_old){
if(deps_curr.has(dep_signal)) continue;
removeSignalListener(dep_signal, contextReWatch);
}
};
deps.set(out[mark], contextReWatch);
@ -43,46 +43,46 @@ export function observable(value, actions){
contextReWatch();
return out;
}
export { observable as O };
observable.action= function(o, name, ...a){
const s= o[mark], { actions }= s;
export { signal as S };
signal.action= function(s, name, ...a){
const M= s[mark], { actions }= M;
if(!actions || !(name in actions))
throw new Error(`'${o}' has no action with name '${name}'!`);
actions[name].apply(s, a);
if(s.skip) return (delete s.skip);
s.listeners.forEach(l=> l(s.value));
throw new Error(`'${s}' has no action with name '${name}'!`);
actions[name].apply(M, a);
if(M.skip) return (delete M.skip);
M.listeners.forEach(l=> l(M.value));
};
observable.on= function on(o, listener, options= {}){
signal.on= function on(s, listener, options= {}){
const { signal: as }= options;
if(as && as.aborted) return;
if(Array.isArray(o)) return o.forEach(s=> on(s, listener, options));
addObservableListener(o, listener);
if(as) as.addEventListener("abort", ()=> removeObservableListener(o, listener));
//TODO cleanup when observable removed
if(Array.isArray(s)) return s.forEach(s=> on(s, listener, options));
addSignalListener(s, listener);
if(as) as.addEventListener("abort", ()=> removeSignalListener(s, listener));
//TODO cleanup when signal removed
};
observable.symbols= {
//observable: mark,
onclear: Symbol.for("Observable.onclear")
signal.symbols= {
//signal: mark,
onclear: Symbol.for("Signal.onclear")
};
observable.clear= function(...observables){
for(const o of observables){
const s= o[mark];
if(!s) continue;
delete o.toJSON;
s.onclear.forEach(f=> f.call(s));
clearListDeps(o, s);
delete o[mark];
signal.clear= function(...signals){
for(const s of signals){
const M= s[mark];
if(!M) continue;
delete s.toJSON;
M.onclear.forEach(f=> f.call(M));
clearListDeps(s, M);
delete s[mark];
}
function clearListDeps(o, s){
s.listeners.forEach(l=> {
s.listeners.delete(l);
function clearListDeps(s, o){
o.listeners.forEach(l=> {
o.listeners.delete(l);
if(!deps.has(l)) return;
const ls= deps.get(l);
ls.delete(o);
ls.delete(s);
if(ls.size>1) return;
o.clear(...ls);
s.clear(...ls);
deps.delete(l);
});
}
@ -92,7 +92,7 @@ import { enviroment as env } from "./dom-common.js";
import { el } from "./dom.js";
import { scope } from "./dom.js";
// TODO: third argument for handle `cache_tmp` in re-render
observable.el= function(o, map){
signal.el= function(s, map){
const mark_start= el.mark({ type: "reactive" }, true);
const mark_end= mark_start.end;
const out= env.D.createDocumentFragment();
@ -101,7 +101,7 @@ observable.el= function(o, map){
let cache= {};
const reRenderReactiveElement= v=> {
if(!mark_start.parentNode || !mark_end.parentNode) // === `isConnected` or wasnt yet rendered
return removeObservableListener(o, reRenderReactiveElement);
return removeSignalListener(s, reRenderReactiveElement);
const cache_tmp= cache; // will be reused in the useCache or removed in the while loop on the end
cache= {};
scope.push(current);
@ -128,16 +128,16 @@ observable.el= function(o, map){
if(mark_start.isConnected)
requestCleanUpReactives(current.host());
};
addObservableListener(o, reRenderReactiveElement);
removeObservablesFromElements(o, reRenderReactiveElement, mark_start, map);
reRenderReactiveElement(o());
addSignalListener(s, reRenderReactiveElement);
removeSignalsFromElements(s, reRenderReactiveElement, mark_start, map);
reRenderReactiveElement(s());
return out;
};
function requestCleanUpReactives(host){
if(!host || !host[key_reactive]) return;
(requestIdleCallback || setTimeout)(function(){
host[key_reactive]= host[key_reactive]
.filter(([ o, el ])=> el.isConnected ? true : (removeObservableListener(...o), false));
.filter(([ s, el ])=> el.isConnected ? true : (removeSignalListener(...s), false));
});
}
import { on } from "./events.js";
@ -147,49 +147,49 @@ const observedAttributeActions= {
};
function observedAttribute(store){
return function(instance, name){
const varO= (...args)=> !args.length
? read(varO)
const varS= (...args)=> !args.length
? read(varS)
: instance.setAttribute(name, ...args);
const out= toObservable(varO, instance.getAttribute(name), observedAttributeActions);
const out= toSignal(varS, instance.getAttribute(name), observedAttributeActions);
store[name]= out;
return out;
};
}
const key_attributes= "__dde_attributes";
observable.observedAttributes= function(element){
signal.observedAttributes= function(element){
const store= element[key_attributes]= {};
const attrs= observedAttributes(element, observedAttribute(store));
on.attributeChanged(function attributeChangeToObservable({ detail }){
/*! This maps attributes to observables (`O.observedAttributes`).
on.attributeChanged(function attributeChangeToSignal({ detail }){
/*! This maps attributes to signals (`S.observedAttributes`).
* Investigate `__dde_attributes` key of the element.*/
const [ name, value ]= detail;
const curr= this[key_attributes][name];
if(curr) return observable.action(curr, "_set", value);
if(curr) return signal.action(curr, "_set", value);
})(element);
on.disconnected(function(){
/*! This removes all observables mapped to attributes (`O.observedAttributes`).
/*! This removes all signals mapped to attributes (`S.observedAttributes`).
* Investigate `__dde_attributes` key of the element.*/
observable.clear(...Object.values(this[key_attributes]));
signal.clear(...Object.values(this[key_attributes]));
})(element);
return attrs;
};
import { typeOf } from './helpers.js';
export const observables_config= {
isObservable,
export const signals_config= {
isSignal,
processReactiveAttribute(element, key, attrs, set){
if(!isObservable(attrs)) return attrs;
if(!isSignal(attrs)) return attrs;
const l= attr=> {
if(!element.isConnected)
return removeObservableListener(attrs, l);
return removeSignalListener(attrs, l);
set(key, attr);
};
addObservableListener(attrs, l);
removeObservablesFromElements(attrs, l, element, key);
addSignalListener(attrs, l);
removeSignalsFromElements(attrs, l, element, key);
return attrs();
}
};
function removeObservablesFromElements(o, listener, ...notes){
function removeSignalsFromElements(s, listener, ...notes){
const { current }= scope;
if(current.prevent) return;
current.host(function(element){
@ -197,29 +197,29 @@ function removeObservablesFromElements(o, listener, ...notes){
element[key_reactive]= [];
on.disconnected(()=>
/*!
* Clears all Observables listeners added in the current scope/host (`O.el`, `assign`, …?).
* Clears all Signals listeners added in the current scope/host (`S.el`, `assign`, …?).
* You can investigate the `__dde_reactive` key of the element.
* */
element[key_reactive].forEach(([ [ o, listener ] ])=>
removeObservableListener(o, listener, o[mark] && o[mark].host && o[mark].host() === element))
element[key_reactive].forEach(([ [ s, listener ] ])=>
removeSignalListener(s, listener, s[mark] && s[mark].host && s[mark].host() === element))
)(element);
}
element[key_reactive].push([ [ o, listener ], ...notes ]);
element[key_reactive].push([ [ s, listener ], ...notes ]);
});
}
function create(is_readonly, value, actions){
const varO= is_readonly
? ()=> read(varO)
: (...value)=> value.length ? write(varO, ...value) : read(varO);
return toObservable(varO, value, actions, is_readonly);
const varS= is_readonly
? ()=> read(varS)
: (...value)=> value.length ? write(varS, ...value) : read(varS);
return toSignal(varS, value, actions, is_readonly);
}
const protoSigal= Object.assign(Object.create(null), {
stopPropagation(){
this.skip= true;
}
});
class ObservableDefined extends Error{
class SignalDefined extends Error{
constructor(){
super();
const [ curr, ...rest ]= this.stack.split("\n");
@ -227,66 +227,66 @@ class ObservableDefined extends Error{
this.stack= rest.find(l=> !l.includes(curr_file));
}
}
function toObservable(o, value, actions, readonly= false){
function toSignal(s, value, actions, readonly= false){
const onclear= [];
if(typeOf(actions)!=="[object Object]")
actions= {};
const { onclear: ocs }= observable.symbols;
const { onclear: ocs }= signal.symbols;
if(actions[ocs]){
onclear.push(actions[ocs]);
delete actions[ocs];
}
const { host }= scope;
Reflect.defineProperty(o, mark, {
Reflect.defineProperty(s, mark, {
value: {
value, actions, onclear, host,
listeners: new Set(),
defined: (new ObservableDefined()).stack,
defined: (new SignalDefined()).stack,
readonly
},
enumerable: false,
writable: false,
configurable: true
});
o.toJSON= ()=> o();
o.valueOf= ()=> o[mark] && o[mark].value;
Object.setPrototypeOf(o[mark], protoSigal);
return o;
s.toJSON= ()=> s();
s.valueOf= ()=> s[mark] && s[mark].value;
Object.setPrototypeOf(s[mark], protoSigal);
return s;
}
function currentContext(){
return stack_watch[stack_watch.length - 1];
}
function read(o){
if(!o[mark]) return;
const { value, listeners }= o[mark];
function read(s){
if(!s[mark]) return;
const { value, listeners }= s[mark];
const context= currentContext();
if(context) listeners.add(context);
if(deps.has(context)) deps.get(context).add(o);
if(deps.has(context)) deps.get(context).add(s);
return value;
}
function write(o, value, force){
if(!o[mark]) return;
const s= o[mark];
if(!force && s.value===value) return;
s.value= value;
s.listeners.forEach(l=> l(value));
function write(s, value, force){
if(!s[mark]) return;
const M= s[mark];
if(!force && M.value===value) return;
M.value= value;
M.listeners.forEach(l=> l(value));
return value;
}
function addObservableListener(o, listener){
if(!o[mark]) return;
return o[mark].listeners.add(listener);
function addSignalListener(s, listener){
if(!s[mark]) return;
return s[mark].listeners.add(listener);
}
function removeObservableListener(o, listener, clear_when_empty){
const s= o[mark];
if(!s) return;
const out= s.listeners.delete(listener);
if(clear_when_empty && !s.listeners.size){
observable.clear(o);
if(!deps.has(s)) return out;
const c= deps.get(s);
function removeSignalListener(s, listener, clear_when_empty){
const M= s[mark];
if(!M) return;
const out= M.listeners.delete(listener);
if(clear_when_empty && !M.listeners.size){
signal.clear(s);
if(!deps.has(M)) return out;
const c= deps.get(M);
if(!deps.has(c)) return out;
deps.get(c).forEach(sig=> removeObservableListener(sig, c, true));
deps.get(c).forEach(sig=> removeSignalListener(sig, c, true));
}
return out;
}

13
src/signals-common.js Normal file
View File

@ -0,0 +1,13 @@
export const signals_global= {
isSignal(attributes){ return false; },
processReactiveAttribute(obj, key, attr, set){ return attr; },
};
export function registerReactivity(def, global= true){
if(global) return Object.assign(signals_global, def);
Object.setPrototypeOf(def, signals_global);
return def;
}
/** @param {unknown} _this @returns {typeof signals_global} */
export function signals(_this){
return signals_global.isPrototypeOf(_this) && _this!==signals_global ? _this : signals_global;
}

4
src/signals-lib.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
import type { Action, Actions, signal as s, Signal, SymbolOnclear } from "../signals.d.ts";
export { Action, Actions, Signal, SymbolOnclear };
export const S: s;
export const signal: s;

292
src/signals-lib.js Normal file
View File

@ -0,0 +1,292 @@
export const mark= "__dde_signal";
import { hasOwn } from "./helpers.js";
export function isSignal(candidate){
try{ return hasOwn(candidate, mark); }
catch(e){ return false; }
}
/** @type {function[]} */
const stack_watch= [];
/**
* ### `WeakMap<function, Set<ddeSignal<any, any>>>`
* The `Set` is in the form of `[ source, ...depended signals (DSs) ]`.
* When the DS is cleaned (`S.clear`) it is removed from DSs,
* if remains only one (`source`) it is cleared too.
* ### `WeakMap<object, function>`
* This is used for revesed deps, the `function` is also key for `deps`.
* @type {WeakMap<function|object,Set<ddeSignal<any, any>>|function>}
* */
const deps= new WeakMap();
export function signal(value, actions){
if(typeof value!=="function")
return create(false, value, actions);
if(isSignal(value)) return value;
const out= create(true);
const contextReWatch= function(){
const [ origin, ...deps_old ]= deps.get(contextReWatch);
deps.set(contextReWatch, new Set([ origin ]));
stack_watch.push(contextReWatch);
write(out, value());
stack_watch.pop();
if(!deps_old.length) return;
const deps_curr= deps.get(contextReWatch);
for (const dep_signal of deps_old){
if(deps_curr.has(dep_signal)) continue;
removeSignalListener(dep_signal, contextReWatch);
}
};
deps.set(out[mark], contextReWatch);
deps.set(contextReWatch, new Set([ out ]));
contextReWatch();
return out;
}
export { signal as S };
signal.action= function(s, name, ...a){
const M= s[mark], { actions }= M;
if(!actions || !(name in actions))
throw new Error(`'${s}' has no action with name '${name}'!`);
actions[name].apply(M, a);
if(M.skip) return (delete M.skip);
M.listeners.forEach(l=> l(M.value));
};
signal.on= function on(s, listener, options= {}){
const { signal: as }= options;
if(as && as.aborted) return;
if(Array.isArray(s)) return s.forEach(s=> on(s, listener, options));
addSignalListener(s, listener);
if(as) as.addEventListener("abort", ()=> removeSignalListener(s, listener));
//TODO cleanup when signal removed
};
signal.symbols= {
//signal: mark,
onclear: Symbol.for("Signal.onclear")
};
signal.clear= function(...signals){
for(const s of signals){
const M= s[mark];
if(!M) continue;
delete s.toJSON;
M.onclear.forEach(f=> f.call(M));
clearListDeps(s, M);
delete s[mark];
}
function clearListDeps(s, o){
o.listeners.forEach(l=> {
o.listeners.delete(l);
if(!deps.has(l)) return;
const ls= deps.get(l);
ls.delete(s);
if(ls.size>1) return;
s.clear(...ls);
deps.delete(l);
});
}
};
const key_reactive= "__dde_reactive";
import { enviroment as env } from "./dom-common.js";
import { el } from "./dom.js";
import { scope } from "./dom.js";
// TODO: third argument for handle `cache_tmp` in re-render
signal.el= function(s, map){
const mark_start= el.mark({ type: "reactive" }, true);
const mark_end= mark_start.end;
const out= env.D.createDocumentFragment();
out.append(mark_start, mark_end);
const { current }= scope;
let cache= {};
const reRenderReactiveElement= v=> {
if(!mark_start.parentNode || !mark_end.parentNode) // === `isConnected` or wasnt yet rendered
return removeSignalListener(s, reRenderReactiveElement);
const cache_tmp= cache; // will be reused in the useCache or removed in the while loop on the end
cache= {};
scope.push(current);
let els= map(v, function useCache(key, fun){
let value;
if(hasOwn(cache_tmp, key)){
value= cache_tmp[key];
delete cache_tmp[key];
} else
value= fun();
cache[key]= value;
return value;
});
scope.pop();
if(!Array.isArray(els))
els= [ els ];
const el_start_rm= document.createComment("");
els.push(el_start_rm);
mark_start.after(...els);
let el_r;
while(( el_r= el_start_rm.nextSibling ) && el_r !== mark_end)
el_r.remove();
el_start_rm.remove();
if(mark_start.isConnected)
requestCleanUpReactives(current.host());
};
addSignalListener(s, reRenderReactiveElement);
removeSignalsFromElements(s, reRenderReactiveElement, mark_start, map);
reRenderReactiveElement(s());
return out;
};
function requestCleanUpReactives(host){
if(!host || !host[key_reactive]) return;
(requestIdleCallback || setTimeout)(function(){
host[key_reactive]= host[key_reactive]
.filter(([ s, el ])=> el.isConnected ? true : (removeSignalListener(...s), false));
});
}
import { on } from "./events.js";
import { observedAttributes } from "./helpers.js";
const observedAttributeActions= {
_set(value){ this.value= value; },
};
function observedAttribute(store){
return function(instance, name){
const varS= (...args)=> !args.length
? read(varS)
: instance.setAttribute(name, ...args);
const out= toSignal(varS, instance.getAttribute(name), observedAttributeActions);
store[name]= out;
return out;
};
}
const key_attributes= "__dde_attributes";
signal.observedAttributes= function(element){
const store= element[key_attributes]= {};
const attrs= observedAttributes(element, observedAttribute(store));
on.attributeChanged(function attributeChangeToSignal({ detail }){
/*! This maps attributes to signals (`S.observedAttributes`).
* Investigate `__dde_attributes` key of the element.*/
const [ name, value ]= detail;
const curr= this[key_attributes][name];
if(curr) return signal.action(curr, "_set", value);
})(element);
on.disconnected(function(){
/*! This removes all signals mapped to attributes (`S.observedAttributes`).
* Investigate `__dde_attributes` key of the element.*/
signal.clear(...Object.values(this[key_attributes]));
})(element);
return attrs;
};
import { typeOf } from './helpers.js';
export const signals_config= {
isSignal,
processReactiveAttribute(element, key, attrs, set){
if(!isSignal(attrs)) return attrs;
const l= attr=> {
if(!element.isConnected)
return removeSignalListener(attrs, l);
set(key, attr);
};
addSignalListener(attrs, l);
removeSignalsFromElements(attrs, l, element, key);
return attrs();
}
};
function removeSignalsFromElements(s, listener, ...notes){
const { current }= scope;
if(current.prevent) return;
current.host(function(element){
if(!element[key_reactive]){
element[key_reactive]= [];
on.disconnected(()=>
/*!
* Clears all Signals listeners added in the current scope/host (`S.el`, `assign`, …?).
* You can investigate the `__dde_reactive` key of the element.
* */
element[key_reactive].forEach(([ [ s, listener ] ])=>
removeSignalListener(s, listener, s[mark] && s[mark].host && s[mark].host() === element))
)(element);
}
element[key_reactive].push([ [ s, listener ], ...notes ]);
});
}
function create(is_readonly, value, actions){
const varS= is_readonly
? ()=> read(varS)
: (...value)=> value.length ? write(varS, ...value) : read(varS);
return toSignal(varS, value, actions, is_readonly);
}
const protoSigal= Object.assign(Object.create(null), {
stopPropagation(){
this.skip= true;
}
});
class SignalDefined extends Error{
constructor(){
super();
const [ curr, ...rest ]= this.stack.split("\n");
const curr_file= curr.slice(curr.indexOf("@"), curr.indexOf(".js:")+4);
this.stack= rest.find(l=> !l.includes(curr_file));
}
}
function toSignal(s, value, actions, readonly= false){
const onclear= [];
if(typeOf(actions)!=="[object Object]")
actions= {};
const { onclear: ocs }= signal.symbols;
if(actions[ocs]){
onclear.push(actions[ocs]);
delete actions[ocs];
}
const { host }= scope;
Reflect.defineProperty(s, mark, {
value: {
value, actions, onclear, host,
listeners: new Set(),
defined: (new SignalDefined()).stack,
readonly
},
enumerable: false,
writable: false,
configurable: true
});
s.toJSON= ()=> s();
s.valueOf= ()=> s[mark] && s[mark].value;
Object.setPrototypeOf(s[mark], protoSigal);
return s;
}
function currentContext(){
return stack_watch[stack_watch.length - 1];
}
function read(s){
if(!s[mark]) return;
const { value, listeners }= s[mark];
const context= currentContext();
if(context) listeners.add(context);
if(deps.has(context)) deps.get(context).add(s);
return value;
}
function write(s, value, force){
if(!s[mark]) return;
const M= s[mark];
if(!force && M.value===value) return;
M.value= value;
M.listeners.forEach(l=> l(value));
return value;
}
function addSignalListener(s, listener){
if(!s[mark]) return;
return s[mark].listeners.add(listener);
}
function removeSignalListener(s, listener, clear_when_empty){
const M= s[mark];
if(!M) return;
const out= M.listeners.delete(listener);
if(clear_when_empty && !M.listeners.size){
signal.clear(s);
if(!deps.has(M)) return out;
const c= deps.get(M);
if(!deps.has(c)) return out;
deps.get(c).forEach(sig=> removeSignalListener(sig, c, true));
}
return out;
}