@cldmv/slothlet 3.16.0 → 3.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,21 +43,18 @@ Every feature has been hardened with a comprehensive test suite - over **5,300 t
43
43
 
44
44
  ## ✨ What's New
45
45
 
46
- ### Latest: v3.16.0 (September 2026)
46
+ ### Latest: v3.16.2 (September 2026)
47
47
 
48
- - **Stackable lifecycle routines** — a `routines` config option stacks every module's contribution to a named lifecycle-hook-style export (`initialize`, `shutdown`, or any custom name) into one callable at its composed api path, plus a root cascade (`self.<name>()` ≡ `api.slothlet.<name>()`) that runs every matching contribution anywhere in the tree; replaces the deprecated `collectLifecycleHooks` option with bare/dotted, recursive, and root-anchored glob name matching, `manual`/`startup`/`shutdown`/`destroy` modes, and best-effort aggregated error reporting (#341). A new `stackRoutines` flag — decoupled entirely from `collisionMode` — controls whether same-path contributors that lost an ordinary collision still participate in the stack (#365). All opt-in; no breaking API changes.
49
- - **Collision, ownership & routine-stacking correctness pass** — the per-path routine stack now self-heals reactively instead of going stale after a late reassignment or a second colliding contributor (#362, #374); `assignToApiPath` awaits the now-async collision reconciliation it was firing unawaited (#369, #376); and collision-mode overrides, speculative-registration revert, and lazy-collision materialization are threaded correctly throughout `api.add()` (#366, #367, #372). Minimum Node is raised to `>=22.12.0` to match the toolchain floor.
50
- - **Remaining live-runtime deferred-callback boundaries pinned** — `requestAnimationFrame`/`requestIdleCallback`, IDL `on*` handler properties (`EventSource`, `WebSocket`, `Worker`/`MessagePort`, `FileReader`, `XMLHttpRequest`), and the `MutationObserver`/`ResizeObserver`/`IntersectionObserver` constructors now correctly pin `self`/context identity for a browser-mode live-runtime instance, closing out the gaps left after `setTimeout`/`EventTarget.addEventListener` (#349, #350, #352, #353).
51
- - [View full v3.16.0 Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v3/v3.16.0.md)
48
+ - **Routine cascades now resolve `self.*`** — a root-cascade routine call (`api.<name>()` / `api.slothlet.<name>()`, or an auto-fired `startup` / `shutdown` / `destroy` routine) ran its contributors with no active extent, so any contributor reaching ambient `self.*` threw `RUNTIME_NO_ACTIVE_CONTEXT_SELF` while the same contribution called per-path (`api.<path>.<name>()`) worked. Routines now run every contributor inside the instance extent under its own leaf wrapper, so each runs exactly as if called directly — `self.*` resolves and permission/caller identity is the contributor itself, cascade and per-path alike (#394).
49
+ - **Docs build & contributor-guide fixes** — repairs the post-release docs-site build that had failed since v3.16.0 on an unparseable JSDoc `@param` type (now a proper `@callback` typedef, no public type change) (#385), and refreshes `CONTRIBUTING.md` for the v4 flow (#386).
50
+ - [View full v3.16.2 Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v3/v3.16.2.md)
52
51
 
53
52
  ### Recent Releases
54
53
 
54
+ - **v3.16.1** (September 2026) — Widened the optional `typescript` peer to `^6.0.3 || ^7.0.0` so consumers can adopt TypeScript 7 without an `npm ci` `ERESOLVE` conflict against `@cldmv/slothlet`; shipped `.d.ts` verified clean under TypeScript 7.0.2 (#382) ([Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v3/v3.16.1.md))
55
+ - **v3.16.0** (September 2026) — Stackable lifecycle routines (`routines` config + root cascade) replacing the deprecated `collectLifecycleHooks`, a `stackRoutines` flag decoupled from `collisionMode`, a large collision/ownership/routine-stacking correctness pass with reactive per-path stack self-heal, and the remaining live-runtime deferred-callback boundaries pinned; minimum Node raised to `>=22.12.0` (#341, #365, #366, #362, #369) ([Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v3/v3.16.0.md))
55
56
  - **v3.15.3** (September 2026) — Wrap-on-set / `api.add()` live Proxy delegation, plus hook-lifecycle default fixes: `self.X = obj` and `api.slothlet.api.add()` now give a genuinely live, two-way view onto the underlying object instead of a frozen snapshot, including `EventEmitter`-derived values (#340, #342); hook-subset default `||` bug fixed to `??` (#343, #344) ([Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v3/v3.15.3.md))
56
- - **v3.15.2** (September 2026) — Core context/composition bug-fix release: async-leaf class-instance context propagation, wrap-on-set context/permission parity with `add()`, `add()` no longer hangs on a socket or circular value, and an EventEmitter context-patch memory leak fix ([Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v3/v3.15.2.md))
57
- - **v3.15.1** (August 2026) — Maintenance & supply-chain hardening, no runtime changes: least-privilege GitHub Actions token permissions (#319), two high-severity dev-only toolchain advisories cleared (#322), and an OpenSSF Scorecard badge (#320) ([Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v3/v3.15.1.md))
58
- - **v3.15.0** (August 2026) — Character-safe `moduleID`s and scoped `remove(moduleID, apiPath)`: a `moduleID` may contain any character and round-trips through `add`/`leaves`/`remove`/`reload`; scoped removals and synthetic adds now survive `reload()`; plus a prototype-pollution guard on mount paths (#303, #302, #304) ([Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v3/v3.15.0.md))
59
-
60
- 📚 **For complete version history and detailed release notes, see [docs/changelog/](https://github.com/CLDMV/slothlet/tree/master/docs/changelog/) folder.**
57
+ - **v3.15.2** (September 2026) — Core context/composition bug-fix release: async-leaf class-instance context propagation, wrap-on-set context/permission parity with `add()`, `add()` no longer hangs on a socket or circular value, and an EventEmitter context-patch memory leak fix ([Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v3/v3.15.2.md)) 📚 **For complete version history and detailed release notes, see [docs/changelog/](https://github.com/CLDMV/slothlet/tree/master/docs/changelog/) folder.**
61
58
 
62
59
  ---
63
60
 
@@ -14,4 +14,4 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import{ComponentBase}from"#factories/component-base";import{SlothletError}from"@cldmv/slothlet/errors";import{resolveWrapper}from"#handlers/unified-wrapper";import{isFrameworkInternal}from"#handlers/framework-internals";import{compilePattern,expandBraces}from"@cldmv/slothlet/helpers/pattern-matcher";const ROOT_BUILTIN_NAMES=new Set(["shutdown","destroy"]);class RoutineManager extends ComponentBase{static slothletProperty="routineManager";constructor(slothlet){super(slothlet);this.raw=[];this.rawWrappers=new Map;this.recording=true;this.patternCache=new Map}reset(){this.raw=[];this.rawWrappers.clear();this.patternCache.clear()}get#routines(){return this.slothlet.config?.routines??[]}#moduleWrappers(moduleID){let inner=this.rawWrappers.get(moduleID);if(!inner){inner=new Map;this.rawWrappers.set(moduleID,inner)}return inner}#findRoutine(name){return this.#routines.find(routine=>routine.name===name)}#compile(pattern){let matcher=this.patternCache.get(pattern);if(!matcher){matcher=compilePattern(pattern);this.patternCache.set(pattern,matcher)}return matcher}#matches(routine,entry){const{name,recursive}=routine;if(name.startsWith("^")){return this.#compile(name.slice(1))(entry.apiPath)}const endpoint=this.slothlet.handlers.ownership?.getModuleEndpoint(entry.moduleID);if(endpoint===void 0)return false;let relative;if(endpoint==="."||endpoint==="")relative=entry.apiPath;else if(entry.apiPath===endpoint)relative="";else if(entry.apiPath.startsWith(`${endpoint}.`))relative=entry.apiPath.slice(endpoint.length+1);else return false;if(relative==="")return false;if(this.#compile(name)(relative))return true;return recursive&&this.#compile(`**.${name}`)(relative)}#requiresDescent(routine){if(routine.name.startsWith("^"))return true;if(routine.recursive)return true;return routine.name.includes(".")}#isCurrentOwner(entry){const ownership=this.slothlet.handlers.ownership;if(!ownership)return true;const owner=ownership.getCurrentOwner(entry.apiPath);if(!owner)return true;return owner.moduleID===entry.moduleID}#applyStackFilter(entries){if(this.slothlet.config?.stackRoutines)return entries;return entries.filter(entry=>this.#isCurrentOwner(entry))}#contributorsFor(name){const routine=this.#findRoutine(name);if(!routine)return[];return this.#applyStackFilter(this.raw.filter(entry=>this.#matches(routine,entry)))}#groupByPath(entries){const groups=new Map;for(const entry of entries){let group=groups.get(entry.apiPath);if(!group){group=[];groups.set(entry.apiPath,group)}group.push(entry)}return groups}onImplCreated(data){if(!this.recording)return;if(this.#routines.length===0)return;const apiPath=data?.apiPath;if(typeof apiPath!=="string"||apiPath.length===0)return;const moduleID=data.moduleID;const fn=data.wrapper?.__impl;const existingIndex=this.raw.findIndex(e=>e.apiPath===apiPath&&e.moduleID===moduleID);if(typeof fn==="function"&&fn.__slothletRoutineStack===true){return}if(typeof fn!=="function"){if(existingIndex!==-1)this.raw.splice(existingIndex,1);this.rawWrappers.get(moduleID)?.delete(apiPath);return}const entry={apiPath,moduleID,fn};if(existingIndex===-1)this.raw.push(entry);else this.raw[existingIndex]=entry;const wrapper=resolveWrapper(data.impl);if(wrapper)this.#moduleWrappers(moduleID).set(apiPath,wrapper);setImmediate(()=>{this.#reactivelyPatchStack(entry).catch(()=>{})})}async#reactivelyPatchStack(entry){if(this.slothlet.____buildDepth>0)return;const api=this.slothlet.api;if(!api)return;const lastDot=entry.apiPath.lastIndexOf(".");const parentPath=lastDot===-1?"":entry.apiPath.slice(0,lastDot);const key=lastDot===-1?entry.apiPath:entry.apiPath.slice(lastDot+1);if(parentPath===""&&ROOT_BUILTIN_NAMES.has(key))return;let winner=null;let winnerGroup=null;const matchingRoutines=[];for(const routine of this.#routines){let group;try{group=this.#applyStackFilter(this.raw.filter(e=>e.apiPath===entry.apiPath&&this.#matches(routine,e)))}catch{continue}if(group.length===0)continue;matchingRoutines.push(routine);winner=routine;winnerGroup=group}if(!winner)return;const isCascadeSlot=parentPath===""&&key===winner.name;const contested=matchingRoutines.length>1;if(!isCascadeSlot&&winnerGroup.length<2&&!contested)return;const target=await this.#resolveContainer(api,parentPath);if(target===null||target===void 0||typeof target!=="object"&&typeof target!=="function")return;if(this.slothlet.____buildDepth>0)return;if(this.slothlet.api!==api)return;const targetWrapper=resolveWrapper(target);if(targetWrapper&&targetWrapper.____slothletInternal?.invalid)return;let freshWinnerGroup;try{freshWinnerGroup=this.#applyStackFilter(this.raw.filter(e=>e.apiPath===entry.apiPath&&this.#matches(winner,e)))}catch{return}if(freshWinnerGroup.length===0)return;if(!isCascadeSlot&&freshWinnerGroup.length<2&&!contested)return;if(isCascadeSlot){let current2;try{current2=target[key]}catch{return}if(!(typeof current2==="function"&&current2.__slothletRoutineCascade===true&&current2.__slothletRoutineName===winner.name)){this.recording=false;try{target[key]=this.#buildCascadeCallable(winner.name)}catch{}finally{this.recording=true}}if(api.slothlet&&(typeof api.slothlet==="object"||typeof api.slothlet==="function")){let slothletCurrent;try{slothletCurrent=api.slothlet[key]}catch{return}if(!(typeof slothletCurrent==="function"&&slothletCurrent.__slothletRoutineCascade===true&&slothletCurrent.__slothletRoutineName===winner.name)){this.recording=false;try{api.slothlet[key]=this.#buildCascadeCallable(winner.name)}catch{}finally{this.recording=true}}}return}let current;try{current=target[key]}catch{return}if(typeof current==="function"&&current.__slothletRoutineStack===true&&current.__slothletRoutineName===winner.name){return}this.recording=false;try{target[key]=this.#buildStackedCallable(entry.apiPath,winner)}catch{}finally{this.recording=true}}onImplRemoved(data){const apiPath=data?.apiPath;const moduleID=data?.moduleID;if(typeof apiPath!=="string"||!moduleID)return;this.raw=this.raw.filter(e=>!(e.apiPath===apiPath&&e.moduleID===moduleID));this.rawWrappers.get(moduleID)?.delete(apiPath)}pruneSubtree(apiPath,moduleID){const prefix=`${apiPath}.`;const matching=this.raw.filter(e=>e.moduleID===moduleID&&(e.apiPath===apiPath||e.apiPath.startsWith(prefix)));if(matching.length===0)return;this.raw=this.raw.filter(e=>!matching.includes(e));const moduleWrappers=this.rawWrappers.get(moduleID);if(!moduleWrappers)return;for(const entry of matching){this.slothlet.handlers.apiManager?.invalidateSpeculativeWrappers(moduleWrappers.get(entry.apiPath));moduleWrappers.delete(entry.apiPath)}}pruneModule(moduleID){this.raw=this.raw.filter(e=>e.moduleID!==moduleID);const moduleWrappers=this.rawWrappers.get(moduleID);if(!moduleWrappers)return;for(const wrapper of moduleWrappers.values()){this.slothlet.handlers.apiManager?.invalidateSpeculativeWrappers(wrapper)}this.rawWrappers.delete(moduleID)}snapshotRawEntries(moduleID){const snapshot=new Map;const moduleWrappers=this.rawWrappers.get(moduleID);this.raw.forEach((entry,index)=>{if(entry.moduleID===moduleID)snapshot.set(entry.apiPath,{fn:entry.fn,index,wrapper:moduleWrappers?.get(entry.apiPath)})});return snapshot}snapshotRawEntry(apiPath,moduleID){const index=this.raw.findIndex(e=>e.apiPath===apiPath&&e.moduleID===moduleID);if(index===-1)return void 0;return{fn:this.raw[index].fn,index,wrapper:this.rawWrappers.get(moduleID)?.get(apiPath)}}revertRawEntry(apiPath,moduleID,priorEntry){const idx=this.raw.findIndex(e=>e.apiPath===apiPath&&e.moduleID===moduleID);if(priorEntry!==void 0){const entry={apiPath,moduleID,fn:priorEntry.fn};if(idx!==-1)this.raw[idx]=entry;else this.raw.splice(Math.min(priorEntry.index,this.raw.length),0,entry);if(priorEntry.wrapper)this.#moduleWrappers(moduleID).set(apiPath,priorEntry.wrapper);else this.rawWrappers.get(moduleID)?.delete(apiPath)}else{if(idx!==-1)this.raw.splice(idx,1);this.rawWrappers.get(moduleID)?.delete(apiPath)}}revertSpeculativeSubtree(api,moduleID,path,priorEntries,visited=new WeakSet){if(!api||typeof api!=="object"&&typeof api!=="function")return;if(visited.has(api)){return}visited.add(api);const revert=revertPath=>{const prior=priorEntries.get(revertPath);if(prior){const idx=this.raw.findIndex(e=>e.apiPath===revertPath&&e.moduleID===moduleID);if(idx!==-1)this.raw[idx]={apiPath:revertPath,moduleID,fn:prior.fn};else this.raw.splice(Math.min(prior.index,this.raw.length),0,{apiPath:revertPath,moduleID,fn:prior.fn});if(prior.wrapper)this.#moduleWrappers(moduleID).set(revertPath,prior.wrapper);else this.rawWrappers.get(moduleID)?.delete(revertPath)}else{this.raw=this.raw.filter(e=>!(e.apiPath===revertPath&&e.moduleID===moduleID));this.rawWrappers.get(moduleID)?.delete(revertPath)}};if(path){revert(path)}for(const[key,value]of Object.entries(api)){const skipProps=["__metadata","__type","_materialize","_impl","____slothletInternal"];if(skipProps.includes(key)){continue}const childPath=path?`${path}.${key}`:key;if(typeof value==="function"||value&&typeof value==="object"){revert(childPath);if(typeof value==="object"&&!Array.isArray(value)){this.revertSpeculativeSubtree(value,moduleID,childPath,priorEntries,visited)}}}}revertSpeculativeState(moduleID,priorEntries){const currentEntries=this.raw.filter(e=>e.moduleID===moduleID);const allPaths=new Set([...currentEntries.map(e=>e.apiPath),...priorEntries.keys()]);const moduleWrappers=this.rawWrappers.get(moduleID);for(const path of allPaths){const prior=priorEntries.get(path);const currentEntry=currentEntries.find(e=>e.apiPath===path);if(!currentEntry||currentEntry.fn!==prior?.fn){moduleWrappers?.get(path)?.___invalidate();if(prior?.wrapper)moduleWrappers?.set(path,prior.wrapper);else moduleWrappers?.delete(path)}if(prior){const idx=this.raw.findIndex(e=>e.apiPath===path&&e.moduleID===moduleID);if(idx!==-1)this.raw[idx]={apiPath:path,moduleID,fn:prior.fn};else this.raw.splice(Math.min(prior.index,this.raw.length),0,{apiPath:path,moduleID,fn:prior.fn})}else{this.raw=this.raw.filter(e=>!(e.apiPath===path&&e.moduleID===moduleID))}}}async#runEntries(apiPath,entries,args=[]){const lastDot=apiPath.lastIndexOf(".");const parentPath=lastDot===-1?"":apiPath.slice(0,lastDot);const receiver=parentPath===""?this.slothlet.api:await this.#resolveContainer(this.slothlet.api,parentPath);if(receiver===void 0){return{results:[],failures:[]}}const results=[];const failures=[];for(const{moduleID,fn}of entries){try{results.push(await Reflect.apply(fn,receiver,args))}catch(error){failures.push({apiPath,moduleID,error})}}return{results,failures}}#throwAggregate(failures){const failureEntries=failures.map(({apiPath,moduleID})=>{const entry={apiPath,moduleID};Object.defineProperty(entry,"toString",{value:()=>`${apiPath} (${moduleID})`,enumerable:false});return entry});Object.defineProperty(failureEntries,"toString",{value:()=>failureEntries.map(String).join(", "),enumerable:false});throw new SlothletError("ROUTINE_FAILED",{apiPath:failures[0].apiPath,moduleID:failures[0].moduleID,count:failures.length,failures:failureEntries},failures[0].error)}async runPath(apiPath,args=[],routine=null){if(!this.slothlet.api)return void 0;const pathEntries=this.raw.filter(entry=>entry.apiPath===apiPath);const scopedEntries=routine?pathEntries.filter(entry=>this.#matches(routine,entry)):pathEntries;const entries=this.#applyStackFilter(scopedEntries);const{results,failures}=await this.#runEntries(apiPath,entries,args);if(failures.length>0)this.#throwAggregate(failures);return results.length===1?results[0]:results}async#materializeTree(root){if(!root)return;const seen=new Set;const visit=async(obj,depth=0)=>{const objType=typeof obj;if(!obj||objType!=="object"&&objType!=="function"||depth>15||seen.has(obj))return;seen.add(obj);try{if(isFrameworkInternal(obj))return;const wrapper=resolveWrapper(obj);if(wrapper){if(wrapper.____slothletInternal.mode==="lazy"&&!wrapper.____slothletInternal.state.materialized){try{await wrapper._materialize()}catch{return}}for(const key of Object.keys(wrapper)){if(!key.startsWith("____"))await visit(wrapper[key],depth+1)}return}for(const key of Object.keys(obj)){await visit(obj[key],depth+1)}}catch{}};const isApiRoot=root===this.slothlet.api;for(const key of Object.keys(root)){if(isApiRoot&&(key==="slothlet"||key==="shutdown"||key==="destroy"))continue;if(key.startsWith("____"))continue;await visit(root[key])}}async#materializeGlobPath(node,segments){if(node===null||node===void 0||segments.length===0)return;const nodeType=typeof node;if(nodeType!=="object"&&nodeType!=="function")return;const nodeWrapper=resolveWrapper(node);if(nodeWrapper&&nodeWrapper.____slothletInternal.mode==="lazy"&&!nodeWrapper.____slothletInternal.state.materialized){try{await nodeWrapper._materialize()}catch{return}}const[segment,...rest]=segments;if(segment==="**"){await this.#materializeTree(node);return}if(!/[*?]/.test(segment)){let child;try{child=node[segment]}catch{return}await this.#materializeGlobPath(child,rest);return}let keys;try{keys=Object.keys(node)}catch{return}const matches=this.#compile(segment);for(const key of keys){if(!matches(key))continue;let child;try{child=node[key]}catch{continue}await this.#materializeGlobPath(child,rest)}}async#materializeFor(routine){if(!this.#requiresDescent(routine))return;if(routine.name.startsWith("^")){await this.#materializeTree(this.slothlet.api);return}const ownership=this.slothlet.handlers.ownership;const endpoints=ownership?new Set(ownership.moduleEndpoints.values()):new Set;if(!routine.recursive&&!routine.name.startsWith("!")){const segmentChains=expandBraces(routine.name).map(alternative=>alternative.split("."));for(const endpoint of endpoints){const mountRoot=endpoint==="."||endpoint===""?this.slothlet.api:await this.#resolveContainer(this.slothlet.api,endpoint);if(mountRoot===null||mountRoot===void 0)continue;for(const segments of segmentChains){await this.#materializeGlobPath(mountRoot,segments)}}return}if(endpoints.has(".")||endpoints.has("")){await this.#materializeTree(this.slothlet.api);return}for(const endpoint of endpoints){const mountRoot=await this.#resolveContainer(this.slothlet.api,endpoint);await this.#materializeTree(mountRoot)}}#orderPaths(apiPaths,order){if(order!=="depth")return apiPaths;return apiPaths.map((apiPath,index)=>({apiPath,index,depth:apiPath.split(".").length})).sort((a,b)=>b.depth-a.depth||a.index-b.index).map(entry=>entry.apiPath)}async runCascade(name,args=[],skipMaterialize=false){if(!this.slothlet.api)return void 0;const routine=this.#findRoutine(name);if(!routine)return void 0;if(!skipMaterialize)await this.#materializeFor(routine);const groups=this.#groupByPath(this.#contributorsFor(name));const orderedPaths=this.#orderPaths([...groups.keys()],routine.order);const results=[];const failures=[];for(const apiPath of orderedPaths){const outcome=await this.#runEntries(apiPath,groups.get(apiPath),args);results.push(outcome.results.length===1?outcome.results[0]:outcome.results);failures.push(...outcome.failures)}if(failures.length>0)this.#throwAggregate(failures);return results.length===1?results[0]:results}async#runModeRoutines(mode){if(!this.slothlet.config?.autoRoutines)return;const routines=this.#routines.filter(routine=>routine.mode===mode);if(routines.length===0)return;for(const routine of routines){await this.#materializeFor(routine)}await this.rebuildStacks(this.slothlet.api);for(const routine of routines){await this.runCascade(routine.name,[],true)}}async runShutdownModeRoutines(){return this.#runModeRoutines("shutdown")}async runDestroyModeRoutines(){return this.#runModeRoutines("destroy")}async runStartupModeRoutines(){return this.#runModeRoutines("startup")}async#resolveContainer(api,path){if(path==="")return api;let node=api;for(const part of path.split(".")){if(node===null||node===void 0)return void 0;try{node=node[part]}catch{return void 0}const wrapper=resolveWrapper(node);if(wrapper&&wrapper.____slothletInternal.mode==="lazy"&&!wrapper.____slothletInternal.state.materialized){try{await wrapper._materialize()}catch{return void 0}}}return node}#buildStackedCallable(apiPath,routine){const manager=this;const stacked=async function slothletRoutineStack(...args){return manager.runPath(apiPath,args,routine)};Object.defineProperty(stacked,"__slothletRoutineStack",{value:true,enumerable:false});Object.defineProperty(stacked,"__slothletRoutineName",{value:routine.name,enumerable:false});return stacked}#buildCascadeCallable(name){const manager=this;const cascade=async function slothletRoutineCascade(...args){return manager.runCascade(name,args)};Object.defineProperty(cascade,"__slothletRoutineStack",{value:true,enumerable:false});Object.defineProperty(cascade,"__slothletRoutineCascade",{value:true,enumerable:false});Object.defineProperty(cascade,"__slothletRoutineName",{value:name,enumerable:false});return cascade}async rebuildStacks(api){if(!api||typeof api!=="object"&&typeof api!=="function")return;this.recording=false;try{for(const routine of this.#routines){const groups=this.#groupByPath(this.#contributorsFor(routine.name));for(const apiPath of groups.keys()){const lastDot=apiPath.lastIndexOf(".");const parentPath=lastDot===-1?"":apiPath.slice(0,lastDot);const key=lastDot===-1?apiPath:apiPath.slice(lastDot+1);if(parentPath===""&&ROOT_BUILTIN_NAMES.has(key))continue;if(parentPath===""&&key===routine.name)continue;const target=await this.#resolveContainer(api,parentPath);if(target===null||target===void 0||typeof target!=="object"&&typeof target!=="function")continue;try{target[key]=this.#buildStackedCallable(apiPath,routine)}catch{}}if(ROOT_BUILTIN_NAMES.has(routine.name))continue;const cascade=this.#buildCascadeCallable(routine.name);try{api[routine.name]=cascade}catch{}if(api.slothlet&&(typeof api.slothlet==="object"||typeof api.slothlet==="function")){try{api.slothlet[routine.name]=cascade}catch{}}}}finally{this.recording=true}}}export{RoutineManager};
17
+ import{ComponentBase}from"#factories/component-base";import{SlothletError}from"@cldmv/slothlet/errors";import{resolveWrapper}from"#handlers/unified-wrapper";import{isFrameworkInternal}from"#handlers/framework-internals";import{compilePattern,expandBraces}from"@cldmv/slothlet/helpers/pattern-matcher";const ROOT_BUILTIN_NAMES=new Set(["shutdown","destroy"]);class RoutineManager extends ComponentBase{static slothletProperty="routineManager";constructor(slothlet){super(slothlet);this.raw=[];this.rawWrappers=new Map;this.recording=true;this.patternCache=new Map}reset(){this.raw=[];this.rawWrappers.clear();this.patternCache.clear()}get#routines(){return this.slothlet.config?.routines??[]}#moduleWrappers(moduleID){let inner=this.rawWrappers.get(moduleID);if(!inner){inner=new Map;this.rawWrappers.set(moduleID,inner)}return inner}#findRoutine(name){return this.#routines.find(routine=>routine.name===name)}#compile(pattern){let matcher=this.patternCache.get(pattern);if(!matcher){matcher=compilePattern(pattern);this.patternCache.set(pattern,matcher)}return matcher}#matches(routine,entry){const{name,recursive}=routine;if(name.startsWith("^")){return this.#compile(name.slice(1))(entry.apiPath)}const endpoint=this.slothlet.handlers.ownership?.getModuleEndpoint(entry.moduleID);if(endpoint===void 0)return false;let relative;if(endpoint==="."||endpoint==="")relative=entry.apiPath;else if(entry.apiPath===endpoint)relative="";else if(entry.apiPath.startsWith(`${endpoint}.`))relative=entry.apiPath.slice(endpoint.length+1);else return false;if(relative==="")return false;if(this.#compile(name)(relative))return true;return recursive&&this.#compile(`**.${name}`)(relative)}#requiresDescent(routine){if(routine.name.startsWith("^"))return true;if(routine.recursive)return true;return routine.name.includes(".")}#isCurrentOwner(entry){const ownership=this.slothlet.handlers.ownership;if(!ownership)return true;const owner=ownership.getCurrentOwner(entry.apiPath);if(!owner)return true;return owner.moduleID===entry.moduleID}#applyStackFilter(entries){if(this.slothlet.config?.stackRoutines)return entries;return entries.filter(entry=>this.#isCurrentOwner(entry))}#contributorsFor(name){const routine=this.#findRoutine(name);if(!routine)return[];return this.#applyStackFilter(this.raw.filter(entry=>this.#matches(routine,entry)))}#groupByPath(entries){const groups=new Map;for(const entry of entries){let group=groups.get(entry.apiPath);if(!group){group=[];groups.set(entry.apiPath,group)}group.push(entry)}return groups}onImplCreated(data){if(!this.recording)return;if(this.#routines.length===0)return;const apiPath=data?.apiPath;if(typeof apiPath!=="string"||apiPath.length===0)return;const moduleID=data.moduleID;const fn=data.wrapper?.__impl;const existingIndex=this.raw.findIndex(e=>e.apiPath===apiPath&&e.moduleID===moduleID);if(typeof fn==="function"&&fn.__slothletRoutineStack===true){return}if(typeof fn!=="function"){if(existingIndex!==-1)this.raw.splice(existingIndex,1);this.rawWrappers.get(moduleID)?.delete(apiPath);return}const entry={apiPath,moduleID,fn};if(existingIndex===-1)this.raw.push(entry);else this.raw[existingIndex]=entry;const wrapper=resolveWrapper(data.impl);if(wrapper)this.#moduleWrappers(moduleID).set(apiPath,wrapper);setImmediate(()=>{this.#reactivelyPatchStack(entry).catch(()=>{})})}async#reactivelyPatchStack(entry){if(this.slothlet.____buildDepth>0)return;const api=this.slothlet.api;if(!api)return;const lastDot=entry.apiPath.lastIndexOf(".");const parentPath=lastDot===-1?"":entry.apiPath.slice(0,lastDot);const key=lastDot===-1?entry.apiPath:entry.apiPath.slice(lastDot+1);if(parentPath===""&&ROOT_BUILTIN_NAMES.has(key))return;let winner=null;let winnerGroup=null;const matchingRoutines=[];for(const routine of this.#routines){let group;try{group=this.#applyStackFilter(this.raw.filter(e=>e.apiPath===entry.apiPath&&this.#matches(routine,e)))}catch{continue}if(group.length===0)continue;matchingRoutines.push(routine);winner=routine;winnerGroup=group}if(!winner)return;const isCascadeSlot=parentPath===""&&key===winner.name;const contested=matchingRoutines.length>1;if(!isCascadeSlot&&winnerGroup.length<2&&!contested)return;const target=await this.#resolveContainer(api,parentPath);if(target===null||target===void 0||typeof target!=="object"&&typeof target!=="function")return;if(this.slothlet.____buildDepth>0)return;if(this.slothlet.api!==api)return;const targetWrapper=resolveWrapper(target);if(targetWrapper&&targetWrapper.____slothletInternal?.invalid)return;let freshWinnerGroup;try{freshWinnerGroup=this.#applyStackFilter(this.raw.filter(e=>e.apiPath===entry.apiPath&&this.#matches(winner,e)))}catch{return}if(freshWinnerGroup.length===0)return;if(!isCascadeSlot&&freshWinnerGroup.length<2&&!contested)return;if(isCascadeSlot){let current2;try{current2=target[key]}catch{return}if(!(typeof current2==="function"&&current2.__slothletRoutineCascade===true&&current2.__slothletRoutineName===winner.name)){this.recording=false;try{target[key]=this.#buildCascadeCallable(winner.name)}catch{}finally{this.recording=true}}if(api.slothlet&&(typeof api.slothlet==="object"||typeof api.slothlet==="function")){let slothletCurrent;try{slothletCurrent=api.slothlet[key]}catch{return}if(!(typeof slothletCurrent==="function"&&slothletCurrent.__slothletRoutineCascade===true&&slothletCurrent.__slothletRoutineName===winner.name)){this.recording=false;try{api.slothlet[key]=this.#buildCascadeCallable(winner.name)}catch{}finally{this.recording=true}}}return}let current;try{current=target[key]}catch{return}if(typeof current==="function"&&current.__slothletRoutineStack===true&&current.__slothletRoutineName===winner.name){return}this.recording=false;try{target[key]=this.#buildStackedCallable(entry.apiPath,winner)}catch{}finally{this.recording=true}}onImplRemoved(data){const apiPath=data?.apiPath;const moduleID=data?.moduleID;if(typeof apiPath!=="string"||!moduleID)return;this.raw=this.raw.filter(e=>!(e.apiPath===apiPath&&e.moduleID===moduleID));this.rawWrappers.get(moduleID)?.delete(apiPath)}pruneSubtree(apiPath,moduleID){const prefix=`${apiPath}.`;const matching=this.raw.filter(e=>e.moduleID===moduleID&&(e.apiPath===apiPath||e.apiPath.startsWith(prefix)));if(matching.length===0)return;this.raw=this.raw.filter(e=>!matching.includes(e));const moduleWrappers=this.rawWrappers.get(moduleID);if(!moduleWrappers)return;for(const entry of matching){this.slothlet.handlers.apiManager?.invalidateSpeculativeWrappers(moduleWrappers.get(entry.apiPath));moduleWrappers.delete(entry.apiPath)}}pruneModule(moduleID){this.raw=this.raw.filter(e=>e.moduleID!==moduleID);const moduleWrappers=this.rawWrappers.get(moduleID);if(!moduleWrappers)return;for(const wrapper of moduleWrappers.values()){this.slothlet.handlers.apiManager?.invalidateSpeculativeWrappers(wrapper)}this.rawWrappers.delete(moduleID)}snapshotRawEntries(moduleID){const snapshot=new Map;const moduleWrappers=this.rawWrappers.get(moduleID);this.raw.forEach((entry,index)=>{if(entry.moduleID===moduleID)snapshot.set(entry.apiPath,{fn:entry.fn,index,wrapper:moduleWrappers?.get(entry.apiPath)})});return snapshot}snapshotRawEntry(apiPath,moduleID){const index=this.raw.findIndex(e=>e.apiPath===apiPath&&e.moduleID===moduleID);if(index===-1)return void 0;return{fn:this.raw[index].fn,index,wrapper:this.rawWrappers.get(moduleID)?.get(apiPath)}}revertRawEntry(apiPath,moduleID,priorEntry){const idx=this.raw.findIndex(e=>e.apiPath===apiPath&&e.moduleID===moduleID);if(priorEntry!==void 0){const entry={apiPath,moduleID,fn:priorEntry.fn};if(idx!==-1)this.raw[idx]=entry;else this.raw.splice(Math.min(priorEntry.index,this.raw.length),0,entry);if(priorEntry.wrapper)this.#moduleWrappers(moduleID).set(apiPath,priorEntry.wrapper);else this.rawWrappers.get(moduleID)?.delete(apiPath)}else{if(idx!==-1)this.raw.splice(idx,1);this.rawWrappers.get(moduleID)?.delete(apiPath)}}revertSpeculativeSubtree(api,moduleID,path,priorEntries,visited=new WeakSet){if(!api||typeof api!=="object"&&typeof api!=="function")return;if(visited.has(api)){return}visited.add(api);const revert=revertPath=>{const prior=priorEntries.get(revertPath);if(prior){const idx=this.raw.findIndex(e=>e.apiPath===revertPath&&e.moduleID===moduleID);if(idx!==-1)this.raw[idx]={apiPath:revertPath,moduleID,fn:prior.fn};else this.raw.splice(Math.min(prior.index,this.raw.length),0,{apiPath:revertPath,moduleID,fn:prior.fn});if(prior.wrapper)this.#moduleWrappers(moduleID).set(revertPath,prior.wrapper);else this.rawWrappers.get(moduleID)?.delete(revertPath)}else{this.raw=this.raw.filter(e=>!(e.apiPath===revertPath&&e.moduleID===moduleID));this.rawWrappers.get(moduleID)?.delete(revertPath)}};if(path){revert(path)}for(const[key,value]of Object.entries(api)){const skipProps=["__metadata","__type","_materialize","_impl","____slothletInternal"];if(skipProps.includes(key)){continue}const childPath=path?`${path}.${key}`:key;if(typeof value==="function"||value&&typeof value==="object"){revert(childPath);if(typeof value==="object"&&!Array.isArray(value)){this.revertSpeculativeSubtree(value,moduleID,childPath,priorEntries,visited)}}}}revertSpeculativeState(moduleID,priorEntries){const currentEntries=this.raw.filter(e=>e.moduleID===moduleID);const allPaths=new Set([...currentEntries.map(e=>e.apiPath),...priorEntries.keys()]);const moduleWrappers=this.rawWrappers.get(moduleID);for(const path of allPaths){const prior=priorEntries.get(path);const currentEntry=currentEntries.find(e=>e.apiPath===path);if(!currentEntry||currentEntry.fn!==prior?.fn){moduleWrappers?.get(path)?.___invalidate();if(prior?.wrapper)moduleWrappers?.set(path,prior.wrapper);else moduleWrappers?.delete(path)}if(prior){const idx=this.raw.findIndex(e=>e.apiPath===path&&e.moduleID===moduleID);if(idx!==-1)this.raw[idx]={apiPath:path,moduleID,fn:prior.fn};else this.raw.splice(Math.min(prior.index,this.raw.length),0,{apiPath:path,moduleID,fn:prior.fn})}else{this.raw=this.raw.filter(e=>!(e.apiPath===path&&e.moduleID===moduleID))}}}async#runEntries(apiPath,entries,args=[]){const lastDot=apiPath.lastIndexOf(".");const parentPath=lastDot===-1?"":apiPath.slice(0,lastDot);const receiver=parentPath===""?this.slothlet.api:await this.#resolveContainer(this.slothlet.api,parentPath);if(receiver===void 0){return{results:[],failures:[]}}const results=[];const failures=[];const contextManager=this.slothlet.contextManager;const instanceID=this.slothlet.instanceID;const canEnterExtent=contextManager&&typeof contextManager.runInContext==="function"&&contextManager.instances?.has?.(instanceID);for(const{moduleID,fn}of entries){try{if(canEnterExtent){const wrapper=this.rawWrappers.get(moduleID)?.get(apiPath);results.push(await contextManager.runInContext(instanceID,fn,receiver,args,wrapper,true))}else{results.push(await Reflect.apply(fn,receiver,args))}}catch(error){failures.push({apiPath,moduleID,error})}}return{results,failures}}#throwAggregate(failures){const failureEntries=failures.map(({apiPath,moduleID})=>{const entry={apiPath,moduleID};Object.defineProperty(entry,"toString",{value:()=>`${apiPath} (${moduleID})`,enumerable:false});return entry});Object.defineProperty(failureEntries,"toString",{value:()=>failureEntries.map(String).join(", "),enumerable:false});throw new SlothletError("ROUTINE_FAILED",{apiPath:failures[0].apiPath,moduleID:failures[0].moduleID,count:failures.length,failures:failureEntries},failures[0].error)}async runPath(apiPath,args=[],routine=null){if(!this.slothlet.api)return void 0;const pathEntries=this.raw.filter(entry=>entry.apiPath===apiPath);const scopedEntries=routine?pathEntries.filter(entry=>this.#matches(routine,entry)):pathEntries;const entries=this.#applyStackFilter(scopedEntries);const{results,failures}=await this.#runEntries(apiPath,entries,args);if(failures.length>0)this.#throwAggregate(failures);return results.length===1?results[0]:results}async#materializeTree(root){if(!root)return;const seen=new Set;const visit=async(obj,depth=0)=>{const objType=typeof obj;if(!obj||objType!=="object"&&objType!=="function"||depth>15||seen.has(obj))return;seen.add(obj);try{if(isFrameworkInternal(obj))return;const wrapper=resolveWrapper(obj);if(wrapper){if(wrapper.____slothletInternal.mode==="lazy"&&!wrapper.____slothletInternal.state.materialized){try{await wrapper._materialize()}catch{return}}for(const key of Object.keys(wrapper)){if(!key.startsWith("____"))await visit(wrapper[key],depth+1)}return}for(const key of Object.keys(obj)){await visit(obj[key],depth+1)}}catch{}};const isApiRoot=root===this.slothlet.api;for(const key of Object.keys(root)){if(isApiRoot&&(key==="slothlet"||key==="shutdown"||key==="destroy"))continue;if(key.startsWith("____"))continue;await visit(root[key])}}async#materializeGlobPath(node,segments){if(node===null||node===void 0||segments.length===0)return;const nodeType=typeof node;if(nodeType!=="object"&&nodeType!=="function")return;const nodeWrapper=resolveWrapper(node);if(nodeWrapper&&nodeWrapper.____slothletInternal.mode==="lazy"&&!nodeWrapper.____slothletInternal.state.materialized){try{await nodeWrapper._materialize()}catch{return}}const[segment,...rest]=segments;if(segment==="**"){await this.#materializeTree(node);return}if(!/[*?]/.test(segment)){let child;try{child=node[segment]}catch{return}await this.#materializeGlobPath(child,rest);return}let keys;try{keys=Object.keys(node)}catch{return}const matches=this.#compile(segment);for(const key of keys){if(!matches(key))continue;let child;try{child=node[key]}catch{continue}await this.#materializeGlobPath(child,rest)}}async#materializeFor(routine){if(!this.#requiresDescent(routine))return;if(routine.name.startsWith("^")){await this.#materializeTree(this.slothlet.api);return}const ownership=this.slothlet.handlers.ownership;const endpoints=ownership?new Set(ownership.moduleEndpoints.values()):new Set;if(!routine.recursive&&!routine.name.startsWith("!")){const segmentChains=expandBraces(routine.name).map(alternative=>alternative.split("."));for(const endpoint of endpoints){const mountRoot=endpoint==="."||endpoint===""?this.slothlet.api:await this.#resolveContainer(this.slothlet.api,endpoint);if(mountRoot===null||mountRoot===void 0)continue;for(const segments of segmentChains){await this.#materializeGlobPath(mountRoot,segments)}}return}if(endpoints.has(".")||endpoints.has("")){await this.#materializeTree(this.slothlet.api);return}for(const endpoint of endpoints){const mountRoot=await this.#resolveContainer(this.slothlet.api,endpoint);await this.#materializeTree(mountRoot)}}#orderPaths(apiPaths,order){if(order!=="depth")return apiPaths;return apiPaths.map((apiPath,index)=>({apiPath,index,depth:apiPath.split(".").length})).sort((a,b)=>b.depth-a.depth||a.index-b.index).map(entry=>entry.apiPath)}async runCascade(name,args=[],skipMaterialize=false){if(!this.slothlet.api)return void 0;const routine=this.#findRoutine(name);if(!routine)return void 0;if(!skipMaterialize)await this.#materializeFor(routine);const groups=this.#groupByPath(this.#contributorsFor(name));const orderedPaths=this.#orderPaths([...groups.keys()],routine.order);const results=[];const failures=[];for(const apiPath of orderedPaths){const outcome=await this.#runEntries(apiPath,groups.get(apiPath),args);results.push(outcome.results.length===1?outcome.results[0]:outcome.results);failures.push(...outcome.failures)}if(failures.length>0)this.#throwAggregate(failures);return results.length===1?results[0]:results}async#runModeRoutines(mode){if(!this.slothlet.config?.autoRoutines)return;const routines=this.#routines.filter(routine=>routine.mode===mode);if(routines.length===0)return;for(const routine of routines){await this.#materializeFor(routine)}await this.rebuildStacks(this.slothlet.api);for(const routine of routines){await this.runCascade(routine.name,[],true)}}async runShutdownModeRoutines(){return this.#runModeRoutines("shutdown")}async runDestroyModeRoutines(){return this.#runModeRoutines("destroy")}async runStartupModeRoutines(){return this.#runModeRoutines("startup")}async#resolveContainer(api,path){if(path==="")return api;let node=api;for(const part of path.split(".")){if(node===null||node===void 0)return void 0;try{node=node[part]}catch{return void 0}const wrapper=resolveWrapper(node);if(wrapper&&wrapper.____slothletInternal.mode==="lazy"&&!wrapper.____slothletInternal.state.materialized){try{await wrapper._materialize()}catch{return void 0}}}return node}#buildStackedCallable(apiPath,routine){const manager=this;const stacked=async function slothletRoutineStack(...args){return manager.runPath(apiPath,args,routine)};Object.defineProperty(stacked,"__slothletRoutineStack",{value:true,enumerable:false});Object.defineProperty(stacked,"__slothletRoutineName",{value:routine.name,enumerable:false});return stacked}#buildCascadeCallable(name){const manager=this;const cascade=async function slothletRoutineCascade(...args){return manager.runCascade(name,args)};Object.defineProperty(cascade,"__slothletRoutineStack",{value:true,enumerable:false});Object.defineProperty(cascade,"__slothletRoutineCascade",{value:true,enumerable:false});Object.defineProperty(cascade,"__slothletRoutineName",{value:name,enumerable:false});return cascade}async rebuildStacks(api){if(!api||typeof api!=="object"&&typeof api!=="function")return;this.recording=false;try{for(const routine of this.#routines){const groups=this.#groupByPath(this.#contributorsFor(routine.name));for(const apiPath of groups.keys()){const lastDot=apiPath.lastIndexOf(".");const parentPath=lastDot===-1?"":apiPath.slice(0,lastDot);const key=lastDot===-1?apiPath:apiPath.slice(lastDot+1);if(parentPath===""&&ROOT_BUILTIN_NAMES.has(key))continue;if(parentPath===""&&key===routine.name)continue;const target=await this.#resolveContainer(api,parentPath);if(target===null||target===void 0||typeof target!=="object"&&typeof target!=="function")continue;try{target[key]=this.#buildStackedCallable(apiPath,routine)}catch{}}if(ROOT_BUILTIN_NAMES.has(routine.name))continue;const cascade=this.#buildCascadeCallable(routine.name);try{api[routine.name]=cascade}catch{}if(api.slothlet&&(typeof api.slothlet==="object"||typeof api.slothlet==="function")){try{api.slothlet[routine.name]=cascade}catch{}}}}finally{this.recording=true}}}export{RoutineManager};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cldmv/slothlet",
3
- "version": "3.16.0",
3
+ "version": "3.16.2",
4
4
  "moduleVersions": {
5
5
  "lazy": "3.0.0",
6
6
  "eager": "3.0.0",
@@ -295,7 +295,7 @@
295
295
  "@cldmv/slothlet-i18n": "^3.11.0",
296
296
  "@cldmv/slothlet-types": "^3.11.0",
297
297
  "esbuild": "^0.28.0",
298
- "typescript": "^6.0.3"
298
+ "typescript": "^6.0.3 || ^7.0.0"
299
299
  },
300
300
  "peerDependenciesMeta": {
301
301
  "@cldmv/slothlet-i18n": {