1
0
mirror of https://github.com/jaandrle/deka-dom-el synced 2025-07-02 04:32:14 +02:00
This commit is contained in:
2025-02-28 14:00:18 +01:00
parent f53b97a89c
commit 8f2fd5a68c
9 changed files with 711 additions and 23 deletions

View File

@ -1,13 +1,43 @@
/**
* Global signals object with default implementation
* @type {Object}
*/
export const signals_global= {
/**
* Checks if a value is a signal
* @param {any} attributes - Value to check
* @returns {boolean} Whether the value is a signal
*/
isSignal(attributes){ return false; },
/**
* Processes an attribute that might be reactive
* @param {Element} obj - Element that owns the attribute
* @param {string} key - Attribute name
* @param {any} attr - Attribute value
* @param {Function} set - Function to set the attribute
* @returns {any} Processed attribute value
*/
processReactiveAttribute(obj, key, attr, set){ return attr; },
};
/**
* Registers a reactivity implementation
* @param {Object} def - Reactivity implementation
* @param {boolean} [global=true] - Whether to set globally or create a new implementation
* @returns {Object} The registered reactivity implementation
*/
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} */
/**
* Gets the signals implementation from a context
* @param {unknown} _this - Context to check for signals implementation
* @returns {typeof signals_global} Signals implementation
*/
export function signals(_this){
return signals_global.isPrototypeOf(_this) && _this!==signals_global ? _this : signals_global;
}

View File

@ -1,5 +1,13 @@
/**
* Symbol used to identify signals in objects
* @type {string}
*/
export const mark= "__dde_signal";
/**
* Error class for signal definition tracking
* Shows the correct stack trace for debugging signal creation
*/
export class SignalDefined extends Error{
constructor(){
super();
@ -8,10 +16,19 @@ export class SignalDefined extends Error{
this.stack= rest.find(l=> !l.includes(curr_file));
}
}
/**
* Batches signal updates to improve performance
* @type {Function}
*/
export const queueSignalWrite= (()=> {
let pendingSignals= new Set();
let scheduled= false;
/**
* Processes all pending signal updates
* @private
*/
function flushSignals() {
scheduled = false;
for(const signal of pendingSignals){
@ -20,6 +37,11 @@ export const queueSignalWrite= (()=> {
}
pendingSignals.clear();
}
/**
* Queues a signal for update
* @param {Object} s - Signal to queue
*/
return function(s){
pendingSignals.add(s);
if(scheduled) return;

View File

@ -2,12 +2,25 @@ import { SignalDefined, queueSignalWrite, mark } from "./helpers.js";
export { mark };
import { hasOwn } from "../helpers.js";
/**
* Checks if a value is a signal
*
* @param {any} candidate - Value to check
* @returns {boolean} True if the value is a signal
*/
export function isSignal(candidate){
return typeof candidate === "function" && hasOwn(candidate, mark);
}
/** @type {function[]} */
const stack_watch= [];
/**
* Stack for tracking nested signal computations
* @type {function[]}
*/
const stack_watch= [];
/**
* Dependencies tracking map for signals
*
* ### `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,
@ -15,14 +28,26 @@ const stack_watch= [];
* ### `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();
/**
* Creates a new signal or converts a function into a derived signal
*
* @param {any|function} value - Initial value or function that computes the value
* @param {Object} [actions] - Custom actions for the signal
* @returns {function} Signal function
*/
export function signal(value, actions){
if(typeof value!=="function")
return create(false, value, actions);
if(isSignal(value)) return value;
const out= create(true);
/**
* Updates the derived signal when dependencies change
* @private
*/
function contextReWatch(){
const [ origin, ...deps_old ]= deps.get(contextReWatch);
deps.set(contextReWatch, new Set([ origin ]));
@ -43,7 +68,16 @@ export function signal(value, actions){
contextReWatch();
return out;
}
/** Alias for signal */
export { signal as S };
/**
* Calls a custom action on a signal
*
* @param {function} s - Signal to call action on
* @param {string} name - Action name
* @param {...any} a - Arguments to pass to the action
*/
signal.action= function(s, name, ...a){
const M= s[mark];
if(!M) return;
@ -54,6 +88,15 @@ signal.action= function(s, name, ...a){
if(M.skip) return (delete M.skip);
queueSignalWrite(s);
};
/**
* Subscribes a listener to signal changes
*
* @param {function|function[]} s - Signal or array of signals to subscribe to
* @param {function} listener - Callback function receiving signal value
* @param {Object} [options={}] - Subscription options
* @param {AbortSignal} [options.signal] - Signal to abort subscription
*/
signal.on= function on(s, listener, options= {}){
const { signal: as }= options;
if(as && as.aborted) return;
@ -61,10 +104,20 @@ signal.on= function on(s, listener, options= {}){
addSignalListener(s, listener);
if(as) as.addEventListener("abort", ()=> removeSignalListener(s, listener));
};
/**
* Symbol constants for signal internals
*/
signal.symbols= {
//signal: mark,
onclear: Symbol.for("Signal.onclear")
};
/**
* Cleans up signals and their dependencies
*
* @param {...function} signals - Signals to clean up
*/
signal.clear= function(...signals){
for(const s of signals){
const M= s[mark];
@ -74,6 +127,13 @@ signal.clear= function(...signals){
clearListDeps(s, M);
delete s[mark];
}
/**
* Cleans up signal dependencies
* @param {function} s - Signal being cleared
* @param {Object} o - Signal metadata
* @private
*/
function clearListDeps(s, o){
o.listeners.forEach(l=> {
o.listeners.delete(l);
@ -88,13 +148,24 @@ signal.clear= function(...signals){
});
}
};
/** Property key for tracking reactive elements */
const key_reactive= "__dde_reactive";
import { enviroment as env } from "../dom-common.js";
import { el } from "../dom.js";
import { scope } from "../dom.js";
import { on } from "../events.js";
/** Store for memoized values */
const storeMemo= new WeakMap();
/**
* Memoizes a function result by key
*
* @param {string|any} key - Cache key (non-strings will be stringified)
* @param {Function} fun - Function to compute value
* @param {Object} [cache] - Optional explicit cache object
* @returns {any} Cached or computed result
*/
export function memo(key, fun, cache){
if(typeof key!=="string") key= JSON.stringify(key);
if(!cache) {
@ -109,6 +180,13 @@ export function memo(key, fun, cache){
return hasOwn(cache, key) ? cache[key] : (cache[key]= fun());
}
// TODO: third argument for handle `cache_tmp` in re-render
/**
* Creates a reactive DOM element that re-renders when signal changes
*
* @param {function} s - Signal to watch
* @param {Function} map - Function mapping signal value to DOM elements
* @returns {DocumentFragment} Fragment containing reactive elements
*/
signal.el= function(s, map){
const mark_start= el.mark({ type: "reactive" }, true);
const mark_end= mark_start.end;
@ -147,6 +225,12 @@ signal.el= function(s, map){
cache= {}));
return out;
};
/**
* Cleans up reactive elements that are no longer connected
*
* @param {Element} host - Host element containing reactive elements
* @private
*/
function requestCleanUpReactives(host){
if(!host || !host[key_reactive]) return;
(requestIdleCallback || setTimeout)(function(){
@ -155,9 +239,22 @@ function requestCleanUpReactives(host){
});
}
import { observedAttributes } from "../helpers.js";
/**
* Actions for observed attribute signals
* @private
*/
const observedAttributeActions= {
_set(value){ this.value= value; },
};
/**
* Creates a function that returns signals for element attributes
*
* @param {Object} store - Storage object for attribute signals
* @returns {Function} Function creating attribute signals
* @private
*/
function observedAttribute(store){
return function(instance, name){
const varS= (...args)=> !args.length
@ -168,7 +265,15 @@ function observedAttribute(store){
return out;
};
}
/** Property key for storing attribute signals */
const key_attributes= "__dde_attributes";
/**
* Creates signals for observed attributes in custom elements
*
* @param {Element} element - Custom element instance
* @returns {Object} Object with attribute signals
*/
signal.observedAttributes= function(element){
const store= element[key_attributes]= {};
const attrs= observedAttributes(element, observedAttribute(store));
@ -188,8 +293,23 @@ signal.observedAttributes= function(element){
};
import { typeOf } from '../helpers.js';
/**
* Signal configuration for the library
* Implements processReactiveAttribute to handle signal-based attributes
*/
export const signals_config= {
isSignal,
/**
* Processes attributes that might be signals
*
* @param {Element} element - Element with the attribute
* @param {string} key - Attribute name
* @param {any} attrs - Attribute value (possibly a signal)
* @param {Function} set - Function to set attribute value
* @returns {any} Processed attribute value
*/
processReactiveAttribute(element, key, attrs, set){
if(!isSignal(attrs)) return attrs;
const l= attr=> {
@ -202,6 +322,14 @@ export const signals_config= {
return attrs();
}
};
/**
* Registers signal listener for cleanup when element is removed
*
* @param {function} s - Signal to track
* @param {Function} listener - Signal listener
* @param {...any} notes - Additional context information
* @private
*/
function removeSignalsFromElements(s, listener, ...notes){
const { current }= scope;
current.host(function(element){
@ -218,9 +346,22 @@ function removeSignalsFromElements(s, listener, ...notes){
});
}
/**
* Registry for cleaning up signals when they are garbage collected
* @type {FinalizationRegistry}
*/
const cleanUpRegistry = new FinalizationRegistry(function(s){
signal.clear({ [mark]: s });
});
/**
* Creates a new signal function
*
* @param {boolean} is_readonly - Whether the signal is readonly
* @param {any} value - Initial signal value
* @param {Object} actions - Custom actions for the signal
* @returns {function} Signal function
* @private
*/
function create(is_readonly, value, actions){
const varS= is_readonly
? ()=> read(varS)
@ -229,11 +370,29 @@ function create(is_readonly, value, actions){
cleanUpRegistry.register(SI, SI[mark]);
return SI;
}
/**
* Prototype for signal internal objects
* @private
*/
const protoSigal= Object.assign(Object.create(null), {
/**
* Prevents signal propagation
*/
stopPropagation(){
this.skip= true;
}
});
/**
* Transforms a function into a signal
*
* @param {function} s - Function to transform
* @param {any} value - Initial value
* @param {Object} actions - Custom actions
* @param {boolean} [readonly=false] - Whether the signal is readonly
* @returns {function} Signal function
* @private
*/
function toSignal(s, value, actions, readonly= false){
const onclear= [];
if(typeOf(actions)!=="[object Object]")
@ -260,9 +419,22 @@ function toSignal(s, value, actions, readonly= false){
Object.setPrototypeOf(s[mark], protoSigal);
return s;
}
/**
* Gets the current computation context
* @returns {function|undefined} Current context function
* @private
*/
function currentContext(){
return stack_watch[stack_watch.length - 1];
}
/**
* Reads a signal's value and tracks dependencies
*
* @param {function} s - Signal to read
* @returns {any} Signal value
* @private
*/
function read(s){
if(!s[mark]) return;
const { value, listeners }= s[mark];
@ -271,6 +443,16 @@ function read(s){
if(deps.has(context)) deps.get(context).add(s);
return value;
}
/**
* Writes a new value to a signal
*
* @param {function} s - Signal to update
* @param {any} value - New value
* @param {boolean} [force=false] - Force update even if value is unchanged
* @returns {any} The new value
* @private
*/
function write(s, value, force){
const M= s[mark];
if(!M || (!force && M.value===value)) return;
@ -280,10 +462,28 @@ function write(s, value, force){
return value;
}
/**
* Adds a listener to a signal
*
* @param {function} s - Signal to listen to
* @param {Function} listener - Callback function
* @returns {Set} Listener set
* @private
*/
function addSignalListener(s, listener){
if(!s[mark]) return;
return s[mark].listeners.add(listener);
}
/**
* Removes a listener from a signal
*
* @param {function} s - Signal to modify
* @param {Function} listener - Listener to remove
* @param {boolean} [clear_when_empty] - Whether to clear the signal when no listeners remain
* @returns {boolean} Whether the listener was found and removed
* @private
*/
function removeSignalListener(s, listener, clear_when_empty){
const M= s[mark];
if(!M) return;