@cldmv/slothlet 3.15.2 → 3.16.0

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.
Files changed (61) hide show
  1. package/README.md +8 -6
  2. package/dist/lib/builders/api-assignment.mjs +1 -1
  3. package/dist/lib/builders/api_builder.mjs +1 -1
  4. package/dist/lib/builders/builder.mjs +1 -1
  5. package/dist/lib/builders/modes-processor.mjs +1 -1
  6. package/dist/lib/handlers/api-cache-manager.mjs +1 -1
  7. package/dist/lib/handlers/api-manager.mjs +1 -1
  8. package/dist/lib/handlers/hook-manager.mjs +1 -1
  9. package/dist/lib/handlers/module-manager.mjs +1 -1
  10. package/dist/lib/handlers/ownership.mjs +1 -1
  11. package/dist/lib/handlers/routine-manager.mjs +17 -0
  12. package/dist/lib/handlers/unified-wrapper.mjs +1 -1
  13. package/dist/lib/helpers/config.mjs +1 -1
  14. package/dist/lib/helpers/defaults.mjs +17 -0
  15. package/dist/lib/helpers/eventtarget-property-context.mjs +17 -0
  16. package/dist/lib/helpers/observer-context.mjs +17 -0
  17. package/dist/lib/helpers/scheduler-context.mjs +1 -1
  18. package/dist/lib/i18n/languages/en-us.json +2 -0
  19. package/dist/lib/modes/eager.mjs +1 -1
  20. package/dist/lib/modes/lazy.mjs +1 -1
  21. package/dist/lib/processors/flatten.mjs +1 -1
  22. package/dist/lib/processors/loader.mjs +1 -1
  23. package/dist/slothlet.mjs +1 -1
  24. package/index.cjs +20 -0
  25. package/index.mjs +14 -0
  26. package/package.json +8 -7
  27. package/types/stub/devcheck.d.mts +1 -1
  28. package/types/stub/lib/builders/api-assignment.d.mts +130 -2
  29. package/types/stub/lib/builders/api_builder.d.mts +109 -2
  30. package/types/stub/lib/builders/builder.d.mts +87 -2
  31. package/types/stub/lib/builders/modes-processor.d.mts +71 -2
  32. package/types/stub/lib/factories/component-base.d.mts +177 -0
  33. package/types/stub/lib/helpers/caller-pinning.d.mts +22 -2
  34. package/types/stub/lib/helpers/class-instance-wrapper.d.mts +58 -2
  35. package/types/stub/lib/helpers/config.d.mts +321 -2
  36. package/types/stub/lib/helpers/defaults.d.mts +42 -0
  37. package/types/stub/lib/helpers/eventemitter-context.d.mts +31 -2
  38. package/types/stub/lib/helpers/eventtarget-context.d.mts +21 -2
  39. package/types/stub/lib/helpers/eventtarget-property-context.d.mts +23 -0
  40. package/types/stub/lib/helpers/generate-manifest.d.mts +180 -2
  41. package/types/stub/lib/helpers/hint-detector.d.mts +27 -2
  42. package/types/stub/lib/helpers/manifest-resolver.d.mts +101 -2
  43. package/types/stub/lib/helpers/modes-utils.d.mts +35 -2
  44. package/types/stub/lib/helpers/module-discovery.d.mts +81 -2
  45. package/types/stub/lib/helpers/module-manifest-validator.d.mts +37 -2
  46. package/types/stub/lib/helpers/module-sort.d.mts +65 -2
  47. package/types/stub/lib/helpers/observer-context.d.mts +23 -0
  48. package/types/stub/lib/helpers/pattern-matcher.d.mts +44 -2
  49. package/types/stub/lib/helpers/platform.d.mts +111 -2
  50. package/types/stub/lib/helpers/resolve-from-caller.d.mts +33 -2
  51. package/types/stub/lib/helpers/scheduler-context.d.mts +23 -2
  52. package/types/stub/lib/helpers/utilities.d.mts +57 -2
  53. package/types/stub/lib/i18n/translations.d.mts +52 -2
  54. package/types/stub/lib/modes/eager.d.mts +56 -2
  55. package/types/stub/lib/modes/lazy.d.mts +67 -2
  56. package/types/stub/lib/processors/flatten.d.mts +123 -2
  57. package/types/stub/lib/processors/loader.d.mts +83 -2
  58. package/types/stub/lib/processors/type-generator.d.mts +19 -2
  59. package/types/stub/lib/processors/typescript.d.mts +174 -2
  60. package/types/stub/lib/runtime/runtime-asynclocalstorage.d.mts +72 -2
  61. package/types/stub/lib/runtime/runtime-livebindings.d.mts +38 -2
@@ -14,4 +14,4 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import{util}from"@cldmv/slothlet/helpers/platform";import{ComponentBase}from"#factories/component-base";import{compilePattern}from"@cldmv/slothlet/helpers/pattern-matcher";import{normalizeHookConfig}from"@cldmv/slothlet/helpers/config";const ERROR_HOOK_PROCESSED=Symbol.for("@cldmv/slothlet/hook-error-processed");const REPLAY_IDENTITY=Symbol("@cldmv/slothlet/hook-replay-identity");const VERSION_BINDING=Symbol("@cldmv/slothlet/hook-version-binding");class HookManager extends ComponentBase{static slothletProperty="hookManager";#hooks={before:{before:{},primary:{},after:{}},after:{before:{},primary:{},after:{}},always:{before:{},primary:{},after:{}},error:{before:{},primary:{},after:{}}};#byId=new Map;#idCounter=0;#validTypes=new Set(["before","after","always","error"]);#validSubsets=new Set(["before","primary","after"]);constructor(slothlet){super(slothlet);const hookConfig=normalizeHookConfig(slothlet.config?.hook);this.enabled=hookConfig.enabled;this.defaultPattern=hookConfig.pattern||"**";this.suppressErrors=hookConfig.suppressErrors||false;this.pinEnforced=hookConfig.pin;this.enabledPatterns=new Set;this.patternFilterActive=false;if(this.defaultPattern!=="**"){this.enabledPatterns.add(this.defaultPattern);this.patternFilterActive=true}this.hooks=new Map;this.registrationOrder=0;this.reportedErrors=new WeakSet}#globalFilterCache=new Map;#registryEpoch=0;#strategyCache=new Map;#bumpEpoch(){this.#registryEpoch++;this.#strategyCache.clear()}#pathMatchesGlobalFilter(apiPath){if(!this.patternFilterActive){return true}for(const pattern of this.enabledPatterns){let matcher=this.#globalFilterCache.get(pattern);if(!matcher){matcher=this.#compilePattern(pattern);this.#globalFilterCache.set(pattern,matcher)}if(matcher(apiPath)){return true}}return false}on(typePattern,handler,options={}){let{type,pattern}=this.#parseTypePattern(typePattern);if(options.pattern!==void 0){pattern=options.pattern}if(typeof handler!=="function"){throw new this.slothlet.SlothletError("INVALID_HOOK_HANDLER",{receivedType:typeof handler,validationError:true})}const id=options.id||this.#generateId();if(this.#byId.has(id)||this.#isGroupId(id)){throw new this.slothlet.SlothletError("DUPLICATE_HOOK_ID",{id,validationError:true})}const subset=options.subset||"primary";if(!this.#validSubsets.has(subset)){throw new this.slothlet.SlothletError("INVALID_HOOK_SUBSET",{subset,validSubsets:Array.from(this.#validSubsets)})}this.#compilePattern(pattern);const ownerWrapper=this.slothlet.contextManager?.getCallerIdentity?.()?.currentWrapper??null;const replayIdentity=options[REPLAY_IDENTITY]??null;const ownerPath=replayIdentity?replayIdentity.ownerPath:ownerWrapper?.____slothletInternal?.apiPath??null;const ownerFilePath=replayIdentity?replayIdentity.ownerFilePath:ownerWrapper?.____slothletInternal?.filePath??null;const versionBinding=options[VERSION_BINDING]??null;if((options.versioned===true||typeof options.versionDispatcher==="function")&&!versionBinding){const versionManager=this.slothlet.handlers?.versionManager;const logicalPath=versionManager?.findLogicalPathFor?.(pattern)??null;const registered=logicalPath?versionManager.list(logicalPath):void 0;if(!registered){throw new this.SlothletError("HOOK_VERSION_UNRESOLVED",{pattern},null,{validationError:true})}const allVersions=versionManager.buildAllVersionsArg(logicalPath);const callerArg=versionManager.buildCallerArg(ownerWrapper);let selected;if(typeof options.versionDispatcher==="function"){try{selected=options.versionDispatcher(allVersions,callerArg)}catch{selected=null}}else{selected=versionManager.resolveForPath(logicalPath,allVersions,callerArg)}const tags=selected==null?[]:Array.isArray(selected)?[...new Set(selected)]:[selected];if(tags.length===0){const defaultTag=versionManager.getDefaultVersion(logicalPath);if(defaultTag==null){throw new this.SlothletError("HOOK_VERSION_UNRESOLVED",{pattern},null,{validationError:true})}tags.push(defaultTag)}for(const tag of tags){if(typeof tag!=="string"||!Object.hasOwn(registered.versions,tag)){throw new this.SlothletError("HOOK_VERSION_UNKNOWN_TAG",{pattern,version:String(tag)},null,{validationError:true})}}const groupId=id;const memberIds=[];try{for(const tag of tags){memberIds.push(this.on(`${tag}.${pattern}:${type}`,handler,{...options,pattern:void 0,versioned:void 0,versionDispatcher:void 0,id:`${groupId}::${tag}`,[VERSION_BINDING]:{groupId,version:tag}}))}}catch(err){for(const memberId of memberIds)this.remove({id:memberId});throw err}return groupId}const permissionManager=this.slothlet.handlers?.permissionManager;if(permissionManager?.isEnabled?.()&&ownerPath&&!this.#isGlobPattern(pattern)){const runtimeContext=this.slothlet.contextManager?.tryGetContext?.()?.context??null;if(!permissionManager.enforceHookAccess(ownerPath,pattern,type,ownerFilePath,null,runtimeContext)){throw new this.slothlet.SlothletError("PERMISSION_DENIED",{caller:ownerPath,target:`${pattern}:${type}`})}}let lockCaller=options.lockCaller!==false;if(!lockCaller&&ownerWrapper&&this.pinEnforced){lockCaller=true;if(!this.slothlet.config?.silent){new this.SlothletWarning("HOOK_UNPINNED_IGNORED",{pattern})}}const handlerIsAsync=options.async===true||util.types.isAsyncFunction(handler._slothletOriginal??handler);const effectiveHandler=lockCaller?this.#pinHandler(handler):handler;const hook={id,type,pattern,handler:effectiveHandler,lockCaller,ownerPath,ownerFilePath,priority:options.priority||0,subset,enabled:true,handlerIsAsync,version:versionBinding?.version,groupId:versionBinding?.groupId,_compiled:null};const typeIndex=this.#hooks[type];const subsetIndex=typeIndex[subset];if(!subsetIndex[pattern]){subsetIndex[pattern]=[]}subsetIndex[pattern].push(hook);this.#byId.set(id,hook);this.#bumpEpoch();return id}#pinHandler(handler){if(typeof handler._slothletOriginal==="function")return handler;const slothlet=this.slothlet;const capturedWrapper=slothlet.contextManager?.getCallerIdentity?.()?.currentWrapper??null;if(!capturedWrapper)return handler;const pinned=function slothlet_pinnedHook(...args){return slothlet.contextManager.runInContext(slothlet.instanceID,handler,this,args,capturedWrapper,true)};pinned._slothletOriginal=handler;return pinned}remove(filter={}){let removed=0;if(filter.id){const hook=this.#byId.get(filter.id);if(hook){this.#removeHook(hook);removed=1;return removed}for(const candidate of[...this.#byId.values()]){if(candidate.groupId===filter.id){this.#removeHook(candidate);removed++}}return removed}const types=filter.type?[filter.type]:Array.from(this.#validTypes);for(const type of types){const typeIndex=this.#hooks[type];for(const subset of["before","primary","after"]){const subsetIndex=typeIndex[subset];if(filter.pattern){const patternHooks=subsetIndex[filter.pattern];if(patternHooks){removed+=patternHooks.length;patternHooks.forEach(hook=>this.#byId.delete(hook.id));delete subsetIndex[filter.pattern]}}else{for(const pattern in subsetIndex){const patternHooks=subsetIndex[pattern];removed+=patternHooks.length;patternHooks.forEach(hook=>this.#byId.delete(hook.id));delete subsetIndex[pattern]}}}}if(removed>0)this.#bumpEpoch();return removed}enable(filter={}){if(typeof filter==="string"){filter={pattern:filter}}this.enabled=true;return this.#setEnabledState(filter,true)}disable(filter={}){if(typeof filter==="string"){filter={pattern:filter}}if(Object.keys(filter).length===0){this.enabled=false}return this.#setEnabledState(filter,false)}enablePattern(pattern){this.#compilePattern(pattern);if(!this.enabledPatterns.has(pattern)){this.enabledPatterns.add(pattern);this.patternFilterActive=true;this.#bumpEpoch()}return this.enabledPatterns.size}disablePattern(pattern){const removed=this.enabledPatterns.delete(pattern);this.#globalFilterCache.delete(pattern);if(this.enabledPatterns.size===0){this.patternFilterActive=false}if(removed){this.#bumpEpoch()}return this.enabledPatterns.size}resetPatternFilter(){this.enabledPatterns.clear();this.#globalFilterCache.clear();this.patternFilterActive=false;if(this.defaultPattern!=="**"){this.enabledPatterns.add(this.defaultPattern);this.patternFilterActive=true}this.#bumpEpoch()}setPinEnforced(value){this.pinEnforced=value===true;return this.pinEnforced}list(filter={}){if(typeof filter==="string"){if(this.#validTypes.has(filter)){filter={type:filter}}else{filter={pattern:filter}}}const hooks=[];if(filter.id){const hook=this.#byId.get(filter.id);if(hook&&(filter.enabled===void 0||hook.enabled===filter.enabled)){hooks.push(this.#serializeHook(hook))}return{registeredHooks:hooks}}const types=filter.type?[filter.type]:Array.from(this.#validTypes);let patternMatcher=null;if(filter.pattern){patternMatcher=this.#compilePattern(filter.pattern)}for(const type of types){const typeIndex=this.#hooks[type];for(const subset of["before","primary","after"]){const subsetIndex=typeIndex[subset];for(const pattern in subsetIndex){const patternHooks=subsetIndex[pattern];for(const hook of patternHooks){if(filter.enabled!==void 0&&hook.enabled!==filter.enabled){continue}if(patternMatcher&&!patternMatcher(hook.pattern)){continue}hooks.push(this.#serializeHook(hook))}}}}return{registeredHooks:hooks}}getHooksForPath(type,apiPath){const hooks=this.#matchHooksForPath(type,apiPath);const permissionManager=this.slothlet.handlers?.permissionManager;if(permissionManager?.isEnabled?.()&&hooks.length>0){const runtimeContext=this.slothlet.contextManager?.tryGetContext?.()?.context??null;return hooks.filter(hook=>permissionManager.checkHookAccess(hook.ownerPath,apiPath,type,hook.ownerFilePath,null,runtimeContext))}return hooks}#matchHooksForPath(type,apiPath){if(this.enabled===false){return[]}if(!this.#pathMatchesGlobalFilter(apiPath)){return[]}const typeIndex=this.#hooks[type];if(!typeIndex){return[]}const hooks=[];for(const subset of["before","primary","after"]){const subsetIndex=typeIndex[subset];const subsetHooks=[];for(const pattern in subsetIndex){const patternHooks=subsetIndex[pattern];if(pattern===apiPath){subsetHooks.push(...patternHooks.filter(h=>h.enabled));continue}for(const hook of patternHooks){if(!hook.enabled)continue;if(!hook._compiled){hook._compiled=this.#compilePattern(hook.pattern)}if(hook._compiled(apiPath)){subsetHooks.push(hook)}}}subsetHooks.sort((a,b)=>b.priority-a.priority);hooks.push(...subsetHooks)}return hooks}getDispatchStrategy(path){const cached=this.#strategyCache.get(path);if(cached&&cached.epoch===this.#registryEpoch){return cached.strategy}const strategy={asyncBefore:this.#matchHooksForPath("before",path).some(hook=>hook.handlerIsAsync),asyncAfter:this.#matchHooksForPath("after",path).some(hook=>hook.handlerIsAsync)};this.#strategyCache.set(path,{epoch:this.#registryEpoch,strategy});return strategy}executeBeforeHooks(path,args,api,ctx){const hooks=this.getHooksForPath("before",path);for(const hook of hooks){try{const result=hook.handler({path,args,api,ctx,version:hook.version});if(result&&typeof result==="object"&&typeof result.then==="function"){throw new this.SlothletError("HOOK_BEFORE_RETURNED_PROMISE",{id:hook.id,path},null,{validationError:true})}if(result!==void 0&&!Array.isArray(result)){return{args,shortCircuit:true,value:result}}if(Array.isArray(result)){args=result}}catch(error){const sourceInfo={type:"before",subset:hook.subset,hookTag:hook.id,hookId:hook.id,timestamp:Date.now(),stack:error.stack};this.executeErrorHooks(path,error,sourceInfo,args,api,ctx);if(!this.suppressErrors){throw error}return{args,shortCircuit:true,value:void 0}}}return{args,shortCircuit:false}}executeAfterHooks(path,result,args,api,ctx){const hooks=this.getHooksForPath("after",path);const originalResult=result;let currentResult=result;for(const hook of hooks){try{const hookContext={path,args,result:currentResult,api,ctx,version:hook.version};const transformed=hook.handler(hookContext);if(transformed&&typeof transformed==="object"&&typeof transformed.then==="function"){throw new this.SlothletError("HOOK_AFTER_RETURNED_PROMISE",{id:hook.id,path},null,{validationError:true})}if(transformed!==void 0){currentResult=transformed}}catch(error){const sourceInfo={type:"after",subset:hook.subset,hookTag:hook.id,hookId:hook.id,timestamp:Date.now(),stack:error.stack};this.executeErrorHooks(path,error,sourceInfo,args,api,ctx);if(!this.suppressErrors){throw error}}}if(currentResult===originalResult){return{modified:false}}else{return{modified:true,result:currentResult}}}async executeBeforeHooksAsync(path,args,api,ctx){const hooks=this.getHooksForPath("before",path);for(const hook of hooks){try{const raw=hook.handler({path,args,api,ctx,version:hook.version});const result=raw&&typeof raw==="object"&&typeof raw.then==="function"?await raw:raw;if(result!==void 0&&!Array.isArray(result)){return{args,shortCircuit:true,value:result}}if(Array.isArray(result)){args=result}}catch(error){const sourceInfo={type:"before",subset:hook.subset,hookTag:hook.id,hookId:hook.id,timestamp:Date.now(),stack:error.stack};this.executeErrorHooks(path,error,sourceInfo,args,api,ctx);if(!this.suppressErrors){throw error}return{args,shortCircuit:true,value:void 0}}}return{args,shortCircuit:false}}async executeAfterHooksAsync(path,result,args,api,ctx){const hooks=this.getHooksForPath("after",path);const originalResult=result;let currentResult=result;for(const hook of hooks){try{const raw=hook.handler({path,args,result:currentResult,api,ctx,version:hook.version});const transformed=raw&&typeof raw==="object"&&typeof raw.then==="function"?await raw:raw;if(transformed!==void 0){currentResult=transformed}}catch(error){const sourceInfo={type:"after",subset:hook.subset,hookTag:hook.id,hookId:hook.id,timestamp:Date.now(),stack:error.stack};this.executeErrorHooks(path,error,sourceInfo,args,api,ctx);if(!this.suppressErrors){throw error}}}if(currentResult===originalResult){return{modified:false}}else{return{modified:true,result:currentResult}}}executeAlwaysHooks(path,args,resultOrError,hasError=false,errors=[],api,ctx){const hooks=this.getHooksForPath("always",path);for(const hook of hooks){try{hook.handler({path,args,result:hasError?void 0:resultOrError,hasError,errors,api,ctx,version:hook.version})}catch(error){const sourceInfo={type:"always",subset:hook.subset,hookTag:hook.id,hookId:hook.id,timestamp:Date.now(),stack:error.stack};this.executeErrorHooks(path,error,sourceInfo,args,api,ctx)}}}executeErrorHooks(path,error,source,args,api,ctx){if(error&&typeof error==="object"){error[ERROR_HOOK_PROCESSED]=true}const hooks=this.getHooksForPath("error",path);for(const hook of hooks){try{hook.handler({path,args,error,errorType:error?.constructor?.name||"Error",source,timestamp:new Date,api,ctx,version:hook.version})}catch(hookError){this.slothlet.debug("hooks",`Error hook failed for ${path}:`,hookError)}}}#parseTypePattern(typePattern){if(typeof typePattern!=="string"){throw new this.slothlet.SlothletError("INVALID_TYPE_PATTERN",{typePattern,expected:"expected a string in the form 'pattern:type' (e.g. 'math.*:before')"})}const firstColon=typePattern.indexOf(":");if(firstColon===-1){throw new this.slothlet.SlothletError("INVALID_TYPE_PATTERN",{typePattern,expected:"missing ':' \u2014 use 'pattern:type' (e.g. 'math.*:before')"})}const lastColon=typePattern.lastIndexOf(":");const trailing=typePattern.substring(lastColon+1);if(this.#validTypes.has(trailing)){const pattern=typePattern.substring(0,lastColon);if(!pattern){throw new this.slothlet.SlothletError("INVALID_TYPE_PATTERN",{typePattern,expected:"empty path pattern \u2014 use 'pattern:type' (e.g. 'math.*:before')"})}return{type:trailing,pattern}}const leading=typePattern.substring(0,firstColon);if(this.#validTypes.has(leading)){const pattern=typePattern.substring(firstColon+1);if(!pattern){throw new this.slothlet.SlothletError("INVALID_TYPE_PATTERN",{typePattern,expected:"empty path pattern \u2014 use 'pattern:type' (e.g. 'math.*:before')"})}if(!this.slothlet.config?.silent){new this.SlothletWarning("HOOK_TYPEPATTERN_PREFIX_DEPRECATED",{given:typePattern,suggested:`${pattern}:${leading}`})}return{type:leading,pattern}}throw new this.slothlet.SlothletError("INVALID_TYPE_PATTERN",{typePattern,expected:`no hook type found \u2014 end with one of: ${Array.from(this.#validTypes).join(", ")} (e.g. 'math.*:before')`})}#compilePattern(pattern){return compilePattern(pattern,{onMaxDepth:maxDepth=>{throw new this.SlothletError("HOOK_BRACE_EXPANSION_MAX_DEPTH",{maxDepth},null,{validationError:true})}})}#isGlobPattern(pattern){return/[*?{!]/.test(pattern)}getCompilePatternForDiagnostics(){return this.#compilePattern.bind(this)}#generateId(){return`hook-${++this.#idCounter}`}#isGroupId(id){for(const hook of this.#byId.values()){if(hook.groupId===id)return true}return false}#removeHook(hook){const typeIndex=this.#hooks[hook.type];const subsetIndex=typeIndex[hook.subset];const patternHooks=subsetIndex[hook.pattern];if(!patternHooks){throw new this.slothlet.SlothletError("INTERNAL_HOOK_STATE_CORRUPT",{hookId:hook.id,type:hook.type,subset:hook.subset,pattern:hook.pattern,detail:"patternHooks array missing from subsetIndex \u2014 #byId and #hooks are desynced"})}const index=patternHooks.indexOf(hook);if(index===-1){throw new this.slothlet.SlothletError("INTERNAL_HOOK_STATE_CORRUPT",{hookId:hook.id,type:hook.type,subset:hook.subset,pattern:hook.pattern,detail:"hook object not found in patternHooks array \u2014 #byId and #hooks are desynced"})}patternHooks.splice(index,1);if(patternHooks.length===0){delete subsetIndex[hook.pattern]}this.#byId.delete(hook.id);this.#bumpEpoch()}#setEnabledState(filter,enabled){let affected=0;let flipped=0;if(filter.id){const hook=this.#byId.get(filter.id);if(hook){if(hook.enabled!==enabled){hook.enabled=enabled;this.#bumpEpoch()}affected=1}return affected}const types=filter.type?[filter.type]:Array.from(this.#validTypes);for(const type of types){const typeIndex=this.#hooks[type];for(const subset of["before","primary","after"]){const subsetIndex=typeIndex[subset];if(filter.pattern){const patternHooks=subsetIndex[filter.pattern]||[];for(const hook of patternHooks){if(hook.enabled!==enabled){hook.enabled=enabled;flipped++}affected++}}else{for(const pattern in subsetIndex){const patternHooks=subsetIndex[pattern];for(const hook of patternHooks){if(hook.enabled!==enabled){hook.enabled=enabled;flipped++}affected++}}}}}if(flipped>0)this.#bumpEpoch();return affected}#serializeHook(hook){return{id:hook.id,type:hook.type,pattern:hook.pattern,priority:hook.priority,subset:hook.subset,enabled:hook.enabled,lockCaller:hook.lockCaller}}exportHooks(){const registrations=[];for(const hook of this.#byId.values()){registrations.push({typePattern:`${hook.pattern}:${hook.type}`,handler:hook.handler,options:{id:hook.id,priority:hook.priority,subset:hook.subset,lockCaller:hook.lockCaller,async:hook.handlerIsAsync},ownerPath:hook.ownerPath,version:hook.version,groupId:hook.groupId,ownerFilePath:hook.ownerFilePath,enabled:hook.enabled})}return registrations}importHooks(registrations){if(!Array.isArray(registrations))return;for(const reg of registrations){this.on(reg.typePattern,reg.handler,{...reg.options,[REPLAY_IDENTITY]:{ownerPath:reg.ownerPath??null,ownerFilePath:reg.ownerFilePath??null},...reg.version!=null?{[VERSION_BINDING]:{groupId:reg.groupId,version:reg.version}}:{}});if(!reg.enabled){this.disable({id:reg.options.id})}}}async shutdown(){this.#hooks={before:{before:{},primary:{},after:{}},after:{before:{},primary:{},after:{}},always:{before:{},primary:{},after:{}},error:{before:{},primary:{},after:{}}};this.#byId.clear();this.#idCounter=0}}export{HookManager};
17
+ import{util}from"@cldmv/slothlet/helpers/platform";import{ComponentBase}from"#factories/component-base";import{compilePattern}from"@cldmv/slothlet/helpers/pattern-matcher";import{normalizeHookConfig}from"@cldmv/slothlet/helpers/config";const HOOK_SUBSETS=Object.freeze(["before","primary","after"]);const DEFAULT_HOOK_SUBSET="primary";const ERROR_HOOK_PROCESSED=Symbol.for("@cldmv/slothlet/hook-error-processed");const REPLAY_IDENTITY=Symbol("@cldmv/slothlet/hook-replay-identity");const VERSION_BINDING=Symbol("@cldmv/slothlet/hook-version-binding");class HookManager extends ComponentBase{static slothletProperty="hookManager";#hooks={before:{before:{},primary:{},after:{}},after:{before:{},primary:{},after:{}},always:{before:{},primary:{},after:{}},error:{before:{},primary:{},after:{}}};#byId=new Map;#idCounter=0;#validTypes=new Set(["before","after","always","error"]);#validSubsets=new Set(HOOK_SUBSETS);constructor(slothlet){super(slothlet);const hookConfig=normalizeHookConfig(slothlet.config?.hook);this.enabled=hookConfig.enabled;this.defaultPattern=hookConfig.pattern||"**";this.suppressErrors=hookConfig.suppressErrors||false;this.pinEnforced=hookConfig.pin;this.enabledPatterns=new Set;this.patternFilterActive=false;if(this.defaultPattern!=="**"){this.enabledPatterns.add(this.defaultPattern);this.patternFilterActive=true}this.hooks=new Map;this.registrationOrder=0;this.reportedErrors=new WeakSet}#globalFilterCache=new Map;#registryEpoch=0;#strategyCache=new Map;#bumpEpoch(){this.#registryEpoch++;this.#strategyCache.clear()}#pathMatchesGlobalFilter(apiPath){if(!this.patternFilterActive){return true}for(const pattern of this.enabledPatterns){let matcher=this.#globalFilterCache.get(pattern);if(!matcher){matcher=this.#compilePattern(pattern);this.#globalFilterCache.set(pattern,matcher)}if(matcher(apiPath)){return true}}return false}on(typePattern,handler,options={}){let{type,pattern}=this.#parseTypePattern(typePattern);if(options.pattern!==void 0){pattern=options.pattern}if(typeof handler!=="function"){throw new this.slothlet.SlothletError("INVALID_HOOK_HANDLER",{receivedType:typeof handler,validationError:true})}const id=options.id||this.#generateId();if(this.#byId.has(id)||this.#isGroupId(id)){throw new this.slothlet.SlothletError("DUPLICATE_HOOK_ID",{id,validationError:true})}const subset=options.subset??DEFAULT_HOOK_SUBSET;if(!this.#validSubsets.has(subset)){throw new this.slothlet.SlothletError("INVALID_HOOK_SUBSET",{subset,validSubsets:Array.from(this.#validSubsets)})}this.#compilePattern(pattern);const ownerWrapper=this.slothlet.contextManager?.getCallerIdentity?.()?.currentWrapper??null;const replayIdentity=options[REPLAY_IDENTITY]??null;const ownerPath=replayIdentity?replayIdentity.ownerPath:ownerWrapper?.____slothletInternal?.apiPath??null;const ownerFilePath=replayIdentity?replayIdentity.ownerFilePath:ownerWrapper?.____slothletInternal?.filePath??null;const versionBinding=options[VERSION_BINDING]??null;if((options.versioned===true||typeof options.versionDispatcher==="function")&&!versionBinding){const versionManager=this.slothlet.handlers?.versionManager;const logicalPath=versionManager?.findLogicalPathFor?.(pattern)??null;const registered=logicalPath?versionManager.list(logicalPath):void 0;if(!registered){throw new this.SlothletError("HOOK_VERSION_UNRESOLVED",{pattern},null,{validationError:true})}const allVersions=versionManager.buildAllVersionsArg(logicalPath);const callerArg=versionManager.buildCallerArg(ownerWrapper);let selected;if(typeof options.versionDispatcher==="function"){try{selected=options.versionDispatcher(allVersions,callerArg)}catch{selected=null}}else{selected=versionManager.resolveForPath(logicalPath,allVersions,callerArg)}const tags=selected==null?[]:Array.isArray(selected)?[...new Set(selected)]:[selected];if(tags.length===0){const defaultTag=versionManager.getDefaultVersion(logicalPath);if(defaultTag==null){throw new this.SlothletError("HOOK_VERSION_UNRESOLVED",{pattern},null,{validationError:true})}tags.push(defaultTag)}for(const tag of tags){if(typeof tag!=="string"||!Object.hasOwn(registered.versions,tag)){throw new this.SlothletError("HOOK_VERSION_UNKNOWN_TAG",{pattern,version:String(tag)},null,{validationError:true})}}const groupId=id;const memberIds=[];try{for(const tag of tags){memberIds.push(this.on(`${tag}.${pattern}:${type}`,handler,{...options,pattern:void 0,versioned:void 0,versionDispatcher:void 0,id:`${groupId}::${tag}`,[VERSION_BINDING]:{groupId,version:tag}}))}}catch(err){for(const memberId of memberIds)this.remove({id:memberId});throw err}return groupId}const permissionManager=this.slothlet.handlers?.permissionManager;if(permissionManager?.isEnabled?.()&&ownerPath&&!this.#isGlobPattern(pattern)){const runtimeContext=this.slothlet.contextManager?.tryGetContext?.()?.context??null;if(!permissionManager.enforceHookAccess(ownerPath,pattern,type,ownerFilePath,null,runtimeContext)){throw new this.slothlet.SlothletError("PERMISSION_DENIED",{caller:ownerPath,target:`${pattern}:${type}`})}}let lockCaller=options.lockCaller!==false;if(!lockCaller&&ownerWrapper&&this.pinEnforced){lockCaller=true;if(!this.slothlet.config?.silent){new this.SlothletWarning("HOOK_UNPINNED_IGNORED",{pattern})}}const handlerIsAsync=options.async===true||util.types.isAsyncFunction(handler._slothletOriginal??handler);const effectiveHandler=lockCaller?this.#pinHandler(handler):handler;const hook={id,type,pattern,handler:effectiveHandler,lockCaller,ownerPath,ownerFilePath,priority:options.priority||0,subset,enabled:true,handlerIsAsync,version:versionBinding?.version,groupId:versionBinding?.groupId,_compiled:null};const typeIndex=this.#hooks[type];const subsetIndex=typeIndex[subset];if(!subsetIndex[pattern]){subsetIndex[pattern]=[]}subsetIndex[pattern].push(hook);this.#byId.set(id,hook);this.#bumpEpoch();return id}#pinHandler(handler){if(typeof handler._slothletOriginal==="function")return handler;const slothlet=this.slothlet;const capturedWrapper=slothlet.contextManager?.getCallerIdentity?.()?.currentWrapper??null;if(!capturedWrapper)return handler;const pinned=function slothlet_pinnedHook(...args){return slothlet.contextManager.runInContext(slothlet.instanceID,handler,this,args,capturedWrapper,true)};pinned._slothletOriginal=handler;return pinned}remove(filter={}){let removed=0;if(filter.id){const hook=this.#byId.get(filter.id);if(hook){this.#removeHook(hook);removed=1;return removed}for(const candidate of[...this.#byId.values()]){if(candidate.groupId===filter.id){this.#removeHook(candidate);removed++}}return removed}const types=filter.type?[filter.type]:Array.from(this.#validTypes);for(const type of types){const typeIndex=this.#hooks[type];for(const subset of HOOK_SUBSETS){const subsetIndex=typeIndex[subset];if(filter.pattern){const patternHooks=subsetIndex[filter.pattern];if(patternHooks){removed+=patternHooks.length;patternHooks.forEach(hook=>this.#byId.delete(hook.id));delete subsetIndex[filter.pattern]}}else{for(const pattern in subsetIndex){const patternHooks=subsetIndex[pattern];removed+=patternHooks.length;patternHooks.forEach(hook=>this.#byId.delete(hook.id));delete subsetIndex[pattern]}}}}if(removed>0)this.#bumpEpoch();return removed}enable(filter={}){if(typeof filter==="string"){filter={pattern:filter}}this.enabled=true;return this.#setEnabledState(filter,true)}disable(filter={}){if(typeof filter==="string"){filter={pattern:filter}}if(Object.keys(filter).length===0){this.enabled=false}return this.#setEnabledState(filter,false)}enablePattern(pattern){this.#compilePattern(pattern);if(!this.enabledPatterns.has(pattern)){this.enabledPatterns.add(pattern);this.patternFilterActive=true;this.#bumpEpoch()}return this.enabledPatterns.size}disablePattern(pattern){const removed=this.enabledPatterns.delete(pattern);this.#globalFilterCache.delete(pattern);if(this.enabledPatterns.size===0){this.patternFilterActive=false}if(removed){this.#bumpEpoch()}return this.enabledPatterns.size}resetPatternFilter(){this.enabledPatterns.clear();this.#globalFilterCache.clear();this.patternFilterActive=false;if(this.defaultPattern!=="**"){this.enabledPatterns.add(this.defaultPattern);this.patternFilterActive=true}this.#bumpEpoch()}setPinEnforced(value){this.pinEnforced=value===true;return this.pinEnforced}list(filter={}){if(typeof filter==="string"){if(this.#validTypes.has(filter)){filter={type:filter}}else{filter={pattern:filter}}}const hooks=[];if(filter.id){const hook=this.#byId.get(filter.id);if(hook&&(filter.enabled===void 0||hook.enabled===filter.enabled)){hooks.push(this.#serializeHook(hook))}return{registeredHooks:hooks}}const types=filter.type?[filter.type]:Array.from(this.#validTypes);let patternMatcher=null;if(filter.pattern){patternMatcher=this.#compilePattern(filter.pattern)}for(const type of types){const typeIndex=this.#hooks[type];for(const subset of HOOK_SUBSETS){const subsetIndex=typeIndex[subset];for(const pattern in subsetIndex){const patternHooks=subsetIndex[pattern];for(const hook of patternHooks){if(filter.enabled!==void 0&&hook.enabled!==filter.enabled){continue}if(patternMatcher&&!patternMatcher(hook.pattern)){continue}hooks.push(this.#serializeHook(hook))}}}}return{registeredHooks:hooks}}getHooksForPath(type,apiPath){const hooks=this.#matchHooksForPath(type,apiPath);const permissionManager=this.slothlet.handlers?.permissionManager;if(permissionManager?.isEnabled?.()&&hooks.length>0){const runtimeContext=this.slothlet.contextManager?.tryGetContext?.()?.context??null;return hooks.filter(hook=>permissionManager.checkHookAccess(hook.ownerPath,apiPath,type,hook.ownerFilePath,null,runtimeContext))}return hooks}#matchHooksForPath(type,apiPath){if(this.enabled===false){return[]}if(!this.#pathMatchesGlobalFilter(apiPath)){return[]}const typeIndex=this.#hooks[type];if(!typeIndex){return[]}const hooks=[];for(const subset of HOOK_SUBSETS){const subsetIndex=typeIndex[subset];const subsetHooks=[];for(const pattern in subsetIndex){const patternHooks=subsetIndex[pattern];if(pattern===apiPath){subsetHooks.push(...patternHooks.filter(h=>h.enabled));continue}for(const hook of patternHooks){if(!hook.enabled)continue;if(!hook._compiled){hook._compiled=this.#compilePattern(hook.pattern)}if(hook._compiled(apiPath)){subsetHooks.push(hook)}}}subsetHooks.sort((a,b)=>b.priority-a.priority);hooks.push(...subsetHooks)}return hooks}getDispatchStrategy(path){const cached=this.#strategyCache.get(path);if(cached&&cached.epoch===this.#registryEpoch){return cached.strategy}const strategy={asyncBefore:this.#matchHooksForPath("before",path).some(hook=>hook.handlerIsAsync),asyncAfter:this.#matchHooksForPath("after",path).some(hook=>hook.handlerIsAsync)};this.#strategyCache.set(path,{epoch:this.#registryEpoch,strategy});return strategy}executeBeforeHooks(path,args,api,ctx){const hooks=this.getHooksForPath("before",path);for(const hook of hooks){try{const result=hook.handler({path,args,api,ctx,version:hook.version});if(result&&typeof result==="object"&&typeof result.then==="function"){throw new this.SlothletError("HOOK_BEFORE_RETURNED_PROMISE",{id:hook.id,path},null,{validationError:true})}if(result!==void 0&&!Array.isArray(result)){return{args,shortCircuit:true,value:result}}if(Array.isArray(result)){args=result}}catch(error){const sourceInfo={type:"before",subset:hook.subset,hookTag:hook.id,hookId:hook.id,timestamp:Date.now(),stack:error.stack};this.executeErrorHooks(path,error,sourceInfo,args,api,ctx);if(!this.suppressErrors){throw error}return{args,shortCircuit:true,value:void 0}}}return{args,shortCircuit:false}}executeAfterHooks(path,result,args,api,ctx){const hooks=this.getHooksForPath("after",path);const originalResult=result;let currentResult=result;for(const hook of hooks){try{const hookContext={path,args,result:currentResult,api,ctx,version:hook.version};const transformed=hook.handler(hookContext);if(transformed&&typeof transformed==="object"&&typeof transformed.then==="function"){throw new this.SlothletError("HOOK_AFTER_RETURNED_PROMISE",{id:hook.id,path},null,{validationError:true})}if(transformed!==void 0){currentResult=transformed}}catch(error){const sourceInfo={type:"after",subset:hook.subset,hookTag:hook.id,hookId:hook.id,timestamp:Date.now(),stack:error.stack};this.executeErrorHooks(path,error,sourceInfo,args,api,ctx);if(!this.suppressErrors){throw error}}}if(currentResult===originalResult){return{modified:false}}else{return{modified:true,result:currentResult}}}async executeBeforeHooksAsync(path,args,api,ctx){const hooks=this.getHooksForPath("before",path);for(const hook of hooks){try{const raw=hook.handler({path,args,api,ctx,version:hook.version});const result=raw&&typeof raw==="object"&&typeof raw.then==="function"?await raw:raw;if(result!==void 0&&!Array.isArray(result)){return{args,shortCircuit:true,value:result}}if(Array.isArray(result)){args=result}}catch(error){const sourceInfo={type:"before",subset:hook.subset,hookTag:hook.id,hookId:hook.id,timestamp:Date.now(),stack:error.stack};this.executeErrorHooks(path,error,sourceInfo,args,api,ctx);if(!this.suppressErrors){throw error}return{args,shortCircuit:true,value:void 0}}}return{args,shortCircuit:false}}async executeAfterHooksAsync(path,result,args,api,ctx){const hooks=this.getHooksForPath("after",path);const originalResult=result;let currentResult=result;for(const hook of hooks){try{const raw=hook.handler({path,args,result:currentResult,api,ctx,version:hook.version});const transformed=raw&&typeof raw==="object"&&typeof raw.then==="function"?await raw:raw;if(transformed!==void 0){currentResult=transformed}}catch(error){const sourceInfo={type:"after",subset:hook.subset,hookTag:hook.id,hookId:hook.id,timestamp:Date.now(),stack:error.stack};this.executeErrorHooks(path,error,sourceInfo,args,api,ctx);if(!this.suppressErrors){throw error}}}if(currentResult===originalResult){return{modified:false}}else{return{modified:true,result:currentResult}}}executeAlwaysHooks(path,args,resultOrError,hasError=false,errors=[],api,ctx){const hooks=this.getHooksForPath("always",path);for(const hook of hooks){try{hook.handler({path,args,result:hasError?void 0:resultOrError,hasError,errors,api,ctx,version:hook.version})}catch(error){const sourceInfo={type:"always",subset:hook.subset,hookTag:hook.id,hookId:hook.id,timestamp:Date.now(),stack:error.stack};this.executeErrorHooks(path,error,sourceInfo,args,api,ctx)}}}executeErrorHooks(path,error,source,args,api,ctx){if(error&&typeof error==="object"){error[ERROR_HOOK_PROCESSED]=true}const hooks=this.getHooksForPath("error",path);for(const hook of hooks){try{hook.handler({path,args,error,errorType:error?.constructor?.name||"Error",source,timestamp:new Date,api,ctx,version:hook.version})}catch(hookError){this.slothlet.debug("hooks",`Error hook failed for ${path}:`,hookError)}}}#parseTypePattern(typePattern){if(typeof typePattern!=="string"){throw new this.slothlet.SlothletError("INVALID_TYPE_PATTERN",{typePattern,expected:"expected a string in the form 'pattern:type' (e.g. 'math.*:before')"})}const firstColon=typePattern.indexOf(":");if(firstColon===-1){throw new this.slothlet.SlothletError("INVALID_TYPE_PATTERN",{typePattern,expected:"missing ':' \u2014 use 'pattern:type' (e.g. 'math.*:before')"})}const lastColon=typePattern.lastIndexOf(":");const trailing=typePattern.substring(lastColon+1);if(this.#validTypes.has(trailing)){const pattern=typePattern.substring(0,lastColon);if(!pattern){throw new this.slothlet.SlothletError("INVALID_TYPE_PATTERN",{typePattern,expected:"empty path pattern \u2014 use 'pattern:type' (e.g. 'math.*:before')"})}return{type:trailing,pattern}}const leading=typePattern.substring(0,firstColon);if(this.#validTypes.has(leading)){const pattern=typePattern.substring(firstColon+1);if(!pattern){throw new this.slothlet.SlothletError("INVALID_TYPE_PATTERN",{typePattern,expected:"empty path pattern \u2014 use 'pattern:type' (e.g. 'math.*:before')"})}if(!this.slothlet.config?.silent){new this.SlothletWarning("HOOK_TYPEPATTERN_PREFIX_DEPRECATED",{given:typePattern,suggested:`${pattern}:${leading}`})}return{type:leading,pattern}}throw new this.slothlet.SlothletError("INVALID_TYPE_PATTERN",{typePattern,expected:`no hook type found \u2014 end with one of: ${Array.from(this.#validTypes).join(", ")} (e.g. 'math.*:before')`})}#compilePattern(pattern){return compilePattern(pattern,{onMaxDepth:maxDepth=>{throw new this.SlothletError("HOOK_BRACE_EXPANSION_MAX_DEPTH",{maxDepth},null,{validationError:true})}})}#isGlobPattern(pattern){return/[*?{!]/.test(pattern)}getCompilePatternForDiagnostics(){return this.#compilePattern.bind(this)}#generateId(){return`hook-${++this.#idCounter}`}#isGroupId(id){for(const hook of this.#byId.values()){if(hook.groupId===id)return true}return false}#removeHook(hook){const typeIndex=this.#hooks[hook.type];const subsetIndex=typeIndex[hook.subset];const patternHooks=subsetIndex[hook.pattern];if(!patternHooks){throw new this.slothlet.SlothletError("INTERNAL_HOOK_STATE_CORRUPT",{hookId:hook.id,type:hook.type,subset:hook.subset,pattern:hook.pattern,detail:"patternHooks array missing from subsetIndex \u2014 #byId and #hooks are desynced"})}const index=patternHooks.indexOf(hook);if(index===-1){throw new this.slothlet.SlothletError("INTERNAL_HOOK_STATE_CORRUPT",{hookId:hook.id,type:hook.type,subset:hook.subset,pattern:hook.pattern,detail:"hook object not found in patternHooks array \u2014 #byId and #hooks are desynced"})}patternHooks.splice(index,1);if(patternHooks.length===0){delete subsetIndex[hook.pattern]}this.#byId.delete(hook.id);this.#bumpEpoch()}#setEnabledState(filter,enabled){let affected=0;let flipped=0;if(filter.id){const hook=this.#byId.get(filter.id);if(hook){if(hook.enabled!==enabled){hook.enabled=enabled;this.#bumpEpoch()}affected=1}return affected}const types=filter.type?[filter.type]:Array.from(this.#validTypes);for(const type of types){const typeIndex=this.#hooks[type];for(const subset of HOOK_SUBSETS){const subsetIndex=typeIndex[subset];if(filter.pattern){const patternHooks=subsetIndex[filter.pattern]||[];for(const hook of patternHooks){if(hook.enabled!==enabled){hook.enabled=enabled;flipped++}affected++}}else{for(const pattern in subsetIndex){const patternHooks=subsetIndex[pattern];for(const hook of patternHooks){if(hook.enabled!==enabled){hook.enabled=enabled;flipped++}affected++}}}}}if(flipped>0)this.#bumpEpoch();return affected}#serializeHook(hook){return{id:hook.id,type:hook.type,pattern:hook.pattern,priority:hook.priority,subset:hook.subset,enabled:hook.enabled,lockCaller:hook.lockCaller}}exportHooks(){const registrations=[];for(const hook of this.#byId.values()){registrations.push({typePattern:`${hook.pattern}:${hook.type}`,handler:hook.handler,options:{id:hook.id,priority:hook.priority,subset:hook.subset,lockCaller:hook.lockCaller,async:hook.handlerIsAsync},ownerPath:hook.ownerPath,version:hook.version,groupId:hook.groupId,ownerFilePath:hook.ownerFilePath,enabled:hook.enabled})}return registrations}importHooks(registrations){if(!Array.isArray(registrations))return;for(const reg of registrations){this.on(reg.typePattern,reg.handler,{...reg.options,[REPLAY_IDENTITY]:{ownerPath:reg.ownerPath??null,ownerFilePath:reg.ownerFilePath??null},...reg.version!=null?{[VERSION_BINDING]:{groupId:reg.groupId,version:reg.version}}:{}});if(!reg.enabled){this.disable({id:reg.options.id})}}}async shutdown(){this.#hooks={before:{before:{},primary:{},after:{}},after:{before:{},primary:{},after:{}},always:{before:{},primary:{},after:{}},error:{before:{},primary:{},after:{}}};this.#byId.clear();this.#idCounter=0}}export{HookManager};
@@ -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{sortModules}from"@cldmv/slothlet/helpers/module-sort";const DEFAULT_MODULE_COLLISION_MODE="merge";class ModuleManager extends ComponentBase{static slothletProperty="moduleManager";#cache=new Map;#mounted=new Map;constructor(slothlet){super(slothlet)}async discover(options={}){await this.#emit("modules:discover-start",{scanRoot:options.scanRoot,options});const{discoverModules}=await import("@cldmv/slothlet/helpers/module-discovery");const found=await discoverModules(options);this.#cache.clear();for(const result of found){const key=`${result.packageName}@${result.manifest.version}`;this.#cache.set(key,result)}await this.#emit("modules:discover-complete",{found,stale:this.getStaleMounts()});return found}sort(results,comparator){return sortModules(results,comparator)}getDiscoveryCache(){return[...this.#cache.values()]}clearDiscoveryCache(){this.#cache.clear()}getStaleMounts(){const stale=[];for(const[key,mountResult]of this.#mounted){if(!this.#cache.has(key)){stale.push(mountResult)}}return stale}async addModule(nameOrResult,options={}){const collisionMode=options.collisionMode??DEFAULT_MODULE_COLLISION_MODE;const discoverResult=await this.#resolveToDiscoverResult(nameOrResult,options);await this.#emit("modules:mount-start",{items:[nameOrResult],options});const mountResult=await this.#mountSingle(discoverResult,collisionMode,null);await this.#emit("modules:loaded",{mounted:[mountResult]});return mountResult}async addModules(items,options={}){if(!Array.isArray(items)){throw new SlothletError("INVALID_ARGUMENT",{argument:"items",expected:"array of string|DiscoverResult",received:typeof items},null,{validationError:true})}const onFailure=options.onFailure??"throw";const concurrency=Math.max(1,Number(options.concurrency)||1);const collisionMode=options.collisionMode??DEFAULT_MODULE_COLLISION_MODE;if(!["throw","rollback","best-effort"].includes(onFailure)){throw new SlothletError("INVALID_ARGUMENT",{argument:"onFailure",expected:"throw|rollback|best-effort",received:String(onFailure)},null,{validationError:true})}const resolved=[];for(const item of items){resolved.push(await this.#resolveToDiscoverResult(item,options))}const versionConfigs=this.#buildVersionConfigs(resolved);await this.#emit("modules:mount-start",{items,options});let outcome;if(concurrency===1){outcome=await this.#mountSerial(resolved,collisionMode,onFailure,versionConfigs)}else{outcome=await this.#mountParallel(resolved,collisionMode,onFailure,concurrency,versionConfigs)}const loadedPayload=Array.isArray(outcome)?{mounted:outcome}:{mounted:outcome.mounted,failed:outcome.failed};await this.#emit("modules:loaded",loadedPayload);return outcome}async removeModule(name,opts={}){const matches=[];for(const[key,mountResult]of this.#mounted){if(mountResult.packageName!==name)continue;if(opts.version!==void 0&&mountResult.discoverResult.manifest.version!==opts.version)continue;matches.push({key,mountResult})}if(matches.length===0)return false;for(const{key,mountResult}of matches){await this.slothlet.handlers.apiManager.removeApiComponent(mountResult.moduleID);this.#mounted.delete(key)}return true}async#resolveToDiscoverResult(arg,options){if(arg&&typeof arg==="object"&&typeof arg.packageName==="string"){return arg}if(typeof arg!=="string"){throw new SlothletError("INVALID_ARGUMENT",{argument:"module name or DiscoverResult",expected:"string or DiscoverResult object",received:typeof arg},null,{validationError:true})}if(this.#cache.size===0){await this.discover(options.discover??{})}const candidates=[];for(const result of this.#cache.values()){if(result.packageName!==arg)continue;if(options.version!==void 0&&result.manifest.version!==options.version)continue;candidates.push(result)}if(candidates.length===0){throw new SlothletError("MODULE_PACKAGE_NOT_FOUND",{packageName:arg,hint:"addModule(name) requires the package to be installed under one of the scanned roots. Run discover() first, or pass a DiscoverResult object directly."},null,{validationError:true})}if(candidates.length>1){throw new SlothletError("INVALID_ARGUMENT",{argument:"version",expected:"version disambiguator (multi-version cache hit)",received:"undefined"},null,{validationError:true})}return candidates[0]}async#mountSingle(discoverResult,collisionMode,versionConfig){const mountPathDotted=discoverResult.mountPath.join(".");const effectiveMountPath=versionConfig?.version?`${versionConfig.version}.${mountPathDotted}`:mountPathDotted;if(collisionMode==="error"){const existing=this.#findExactMountAt(effectiveMountPath);if(existing){throw new SlothletError("MODULE_MOUNT_COLLISION",{packageName:discoverResult.packageName,mountPath:effectiveMountPath,existingModuleID:existing.moduleID,collisionMode},null,{validationError:true})}}const version=discoverResult.manifest.version;const underlyingCollisionMode=collisionMode==="error"?"merge":collisionMode;const moduleID=await this.slothlet.handlers.apiManager.addApiComponent({apiPath:mountPathDotted,folderPath:discoverResult.apiDir,options:{collisionMode:underlyingCollisionMode,metadata:{_module:{manifest:discoverResult.manifest}}},versionConfig:versionConfig??null});const result={packageName:discoverResult.packageName,mountPath:effectiveMountPath,moduleID,discoverResult,versionConfig:versionConfig??null};this.#mounted.set(`${discoverResult.packageName}@${version}`,result);await this.#emit("modules:mount-complete",{name:discoverResult.packageName,version,mountPath:effectiveMountPath,moduleID});return result}#buildVersionConfigs(resolved){const byName=new Map;for(const r of resolved){const list=byName.get(r.packageName)??[];list.push(r);byName.set(r.packageName,list)}const highestByName=new Map;for(const[name,group]of byName){if(group.length<2)continue;const versions=group.map(r=>r.manifest.version);highestByName.set(name,pickHighestSemver(versions))}const configs=new Map;for(let i=0;i<resolved.length;i++){const r=resolved[i];const group=byName.get(r.packageName);if(!group||group.length<2){configs.set(i,null);continue}const versionTag=semverToTag(r.manifest.version);const isDefault=r.manifest.version===highestByName.get(r.packageName);configs.set(i,{version:versionTag,default:isDefault})}return configs}async#emit(event,payload){const lifecycle=this.slothlet?.handlers?.lifecycle;if(lifecycle&&typeof lifecycle.emit==="function"){await lifecycle.emit(event,payload)}}#findExactMountAt(dottedPath){const history=this.slothlet?.handlers?.apiManager?.state?.addHistory;if(!Array.isArray(history))return null;for(let i=history.length-1;i>=0;i--){const entry=history[i];if(entry?.apiPath===dottedPath)return entry}return null}async#mountSerial(resolved,collisionMode,onFailure,versionConfigs){const mounted=[];const failed=[];for(let i=0;i<resolved.length;i++){const item=resolved[i];try{const mountResult=await this.#mountSingle(item,collisionMode,versionConfigs?.get(i)??null);mounted.push(mountResult)}catch(err){if(onFailure==="throw"){throw err}if(onFailure==="rollback"){await this.#rollback(mounted);throw err}failed.push({item,error:err})}}if(onFailure==="best-effort"){return{mounted,failed}}return mounted}async#mountParallel(resolved,collisionMode,onFailure,concurrency,versionConfigs){const mounted=[];const failed=[];let firstError=null;let nextIndex=0;const worker=async()=>{while(true){if(firstError&&onFailure!=="best-effort")return;const i=nextIndex++;if(i>=resolved.length)return;const item=resolved[i];try{const mountResult=await this.#mountSingle(item,collisionMode,versionConfigs?.get(i)??null);mounted.push(mountResult)}catch(err){if(onFailure==="best-effort"){failed.push({item,error:err})}else if(!firstError){firstError=err}}}};const workers=Array.from({length:Math.min(concurrency,resolved.length)},()=>worker());await Promise.all(workers);if(firstError){if(onFailure==="rollback"){await this.#rollback(mounted)}throw firstError}if(onFailure==="best-effort"){return{mounted,failed}}return mounted}async#rollback(mounted){for(const m of mounted){try{await this.slothlet.handlers.apiManager.removeApiComponent(m.moduleID);this.#mounted.delete(`${m.packageName}@${m.discoverResult.manifest.version}`)}catch{}}}}function semverToTag(version){const m=/^(\d+)/.exec(String(version));return m?`v${m[1]}`:`v${version}`}function pickHighestSemver(versions){if(versions.length===1)return versions[0];const segs=v=>String(v).split(/[.\-+]/).map(p=>{const n=Number.parseInt(p,10);return Number.isFinite(n)?n:0});let best=versions[0];let bestSegs=segs(best);for(let i=1;i<versions.length;i++){const candidate=versions[i];const candidateSegs=segs(candidate);const len=Math.max(candidateSegs.length,bestSegs.length);let candidateWins=false;for(let j=0;j<len;j++){const a=candidateSegs[j]??0;const b=bestSegs[j]??0;if(a>b){candidateWins=true;break}if(a<b)break}if(candidateWins){best=candidate;bestSegs=candidateSegs}}return best}export{ModuleManager};
17
+ import{ComponentBase}from"#factories/component-base";import{SlothletError}from"@cldmv/slothlet/errors";import{sortModules}from"@cldmv/slothlet/helpers/module-sort";const DEFAULT_MODULE_COLLISION_MODE="merge";class ModuleManager extends ComponentBase{static slothletProperty="moduleManager";#cache=new Map;#mounted=new Map;constructor(slothlet){super(slothlet)}async discover(options={}){await this.#emit("modules:discover-start",{scanRoot:options.scanRoot,options});const{discoverModules}=await import("@cldmv/slothlet/helpers/module-discovery");const found=await discoverModules(options);this.#cache.clear();for(const result of found){const key=`${result.packageName}@${result.manifest.version}`;this.#cache.set(key,result)}await this.#emit("modules:discover-complete",{found,stale:this.getStaleMounts()});return found}sort(results,comparator){return sortModules(results,comparator)}getDiscoveryCache(){return[...this.#cache.values()]}clearDiscoveryCache(){this.#cache.clear()}getStaleMounts(){const stale=[];for(const[key,mountResult]of this.#mounted){if(!this.#cache.has(key)){stale.push(mountResult)}}return stale}async addModule(nameOrResult,options={}){const collisionMode=options.collisionMode??DEFAULT_MODULE_COLLISION_MODE;const discoverResult=await this.#resolveToDiscoverResult(nameOrResult,options);await this.#emit("modules:mount-start",{items:[nameOrResult],options});const mountResult=await this.#mountSingle(discoverResult,collisionMode,null);await this.slothlet.handlers.routineManager?.rebuildStacks(this.slothlet.api);await this.#emit("modules:loaded",{mounted:[mountResult]});return mountResult}async addModules(items,options={}){if(!Array.isArray(items)){throw new SlothletError("INVALID_ARGUMENT",{argument:"items",expected:"array of string|DiscoverResult",received:typeof items},null,{validationError:true})}const onFailure=options.onFailure??"throw";const concurrency=Math.max(1,Number(options.concurrency)||1);const collisionMode=options.collisionMode??DEFAULT_MODULE_COLLISION_MODE;if(!["throw","rollback","best-effort"].includes(onFailure)){throw new SlothletError("INVALID_ARGUMENT",{argument:"onFailure",expected:"throw|rollback|best-effort",received:String(onFailure)},null,{validationError:true})}const resolved=[];for(const item of items){resolved.push(await this.#resolveToDiscoverResult(item,options))}const versionConfigs=this.#buildVersionConfigs(resolved);await this.#emit("modules:mount-start",{items,options});let outcome;if(concurrency===1){outcome=await this.#mountSerial(resolved,collisionMode,onFailure,versionConfigs)}else{outcome=await this.#mountParallel(resolved,collisionMode,onFailure,concurrency,versionConfigs)}await this.slothlet.handlers.routineManager?.rebuildStacks(this.slothlet.api);const loadedPayload=Array.isArray(outcome)?{mounted:outcome}:{mounted:outcome.mounted,failed:outcome.failed};await this.#emit("modules:loaded",loadedPayload);return outcome}async removeModule(name,opts={}){const matches=[];for(const[key,mountResult]of this.#mounted){if(mountResult.packageName!==name)continue;if(opts.version!==void 0&&mountResult.discoverResult.manifest.version!==opts.version)continue;matches.push({key,mountResult})}if(matches.length===0)return false;for(const{key,mountResult}of matches){await this.slothlet.handlers.apiManager.removeApiComponent(mountResult.moduleID);this.#mounted.delete(key)}return true}async#resolveToDiscoverResult(arg,options){if(arg&&typeof arg==="object"&&typeof arg.packageName==="string"){return arg}if(typeof arg!=="string"){throw new SlothletError("INVALID_ARGUMENT",{argument:"module name or DiscoverResult",expected:"string or DiscoverResult object",received:typeof arg},null,{validationError:true})}if(this.#cache.size===0){await this.discover(options.discover??{})}const candidates=[];for(const result of this.#cache.values()){if(result.packageName!==arg)continue;if(options.version!==void 0&&result.manifest.version!==options.version)continue;candidates.push(result)}if(candidates.length===0){throw new SlothletError("MODULE_PACKAGE_NOT_FOUND",{packageName:arg,hint:"addModule(name) requires the package to be installed under one of the scanned roots. Run discover() first, or pass a DiscoverResult object directly."},null,{validationError:true})}if(candidates.length>1){throw new SlothletError("INVALID_ARGUMENT",{argument:"version",expected:"version disambiguator (multi-version cache hit)",received:"undefined"},null,{validationError:true})}return candidates[0]}async#mountSingle(discoverResult,collisionMode,versionConfig){const mountPathDotted=discoverResult.mountPath.join(".");const effectiveMountPath=versionConfig?.version?`${versionConfig.version}.${mountPathDotted}`:mountPathDotted;if(collisionMode==="error"){const existing=this.#findExactMountAt(effectiveMountPath);if(existing){throw new SlothletError("MODULE_MOUNT_COLLISION",{packageName:discoverResult.packageName,mountPath:effectiveMountPath,existingModuleID:existing.moduleID,collisionMode},null,{validationError:true})}}const version=discoverResult.manifest.version;const underlyingCollisionMode=collisionMode==="error"?"merge":collisionMode;const moduleID=await this.slothlet.handlers.apiManager.addApiComponent({apiPath:mountPathDotted,folderPath:discoverResult.apiDir,options:{collisionMode:underlyingCollisionMode,metadata:{_module:{manifest:discoverResult.manifest}}},versionConfig:versionConfig??null});const result={packageName:discoverResult.packageName,mountPath:effectiveMountPath,moduleID,discoverResult,versionConfig:versionConfig??null};this.#mounted.set(`${discoverResult.packageName}@${version}`,result);await this.#emit("modules:mount-complete",{name:discoverResult.packageName,version,mountPath:effectiveMountPath,moduleID});return result}#buildVersionConfigs(resolved){const byName=new Map;for(const r of resolved){const list=byName.get(r.packageName)??[];list.push(r);byName.set(r.packageName,list)}const highestByName=new Map;for(const[name,group]of byName){if(group.length<2)continue;const versions=group.map(r=>r.manifest.version);highestByName.set(name,pickHighestSemver(versions))}const configs=new Map;for(let i=0;i<resolved.length;i++){const r=resolved[i];const group=byName.get(r.packageName);if(!group||group.length<2){configs.set(i,null);continue}const versionTag=semverToTag(r.manifest.version);const isDefault=r.manifest.version===highestByName.get(r.packageName);configs.set(i,{version:versionTag,default:isDefault})}return configs}async#emit(event,payload){const lifecycle=this.slothlet?.handlers?.lifecycle;if(lifecycle&&typeof lifecycle.emit==="function"){await lifecycle.emit(event,payload)}}#findExactMountAt(dottedPath){const history=this.slothlet?.handlers?.apiManager?.state?.addHistory;if(!Array.isArray(history))return null;for(let i=history.length-1;i>=0;i--){const entry=history[i];if(entry?.apiPath===dottedPath)return entry}return null}async#mountSerial(resolved,collisionMode,onFailure,versionConfigs){const mounted=[];const failed=[];for(let i=0;i<resolved.length;i++){const item=resolved[i];try{const mountResult=await this.#mountSingle(item,collisionMode,versionConfigs?.get(i)??null);mounted.push(mountResult)}catch(err){if(onFailure==="throw"){throw err}if(onFailure==="rollback"){await this.#rollback(mounted);throw err}failed.push({item,error:err})}}if(onFailure==="best-effort"){return{mounted,failed}}return mounted}async#mountParallel(resolved,collisionMode,onFailure,concurrency,versionConfigs){const mounted=[];const failed=[];let firstError=null;let nextIndex=0;const worker=async()=>{while(true){if(firstError&&onFailure!=="best-effort")return;const i=nextIndex++;if(i>=resolved.length)return;const item=resolved[i];try{const mountResult=await this.#mountSingle(item,collisionMode,versionConfigs?.get(i)??null);mounted.push(mountResult)}catch(err){if(onFailure==="best-effort"){failed.push({item,error:err})}else if(!firstError){firstError=err}}}};const workers=Array.from({length:Math.min(concurrency,resolved.length)},()=>worker());await Promise.all(workers);if(firstError){if(onFailure==="rollback"){await this.#rollback(mounted)}throw firstError}if(onFailure==="best-effort"){return{mounted,failed}}return mounted}async#rollback(mounted){for(const m of mounted){try{await this.slothlet.handlers.apiManager.removeApiComponent(m.moduleID);this.#mounted.delete(`${m.packageName}@${m.discoverResult.manifest.version}`)}catch{}}}}function semverToTag(version){const m=/^(\d+)/.exec(String(version));return m?`v${m[1]}`:`v${version}`}function pickHighestSemver(versions){if(versions.length===1)return versions[0];const segs=v=>String(v).split(/[.\-+]/).map(p=>{const n=Number.parseInt(p,10);return Number.isFinite(n)?n:0});let best=versions[0];let bestSegs=segs(best);for(let i=1;i<versions.length;i++){const candidate=versions[i];const candidateSegs=segs(candidate);const len=Math.max(candidateSegs.length,bestSegs.length);let candidateWins=false;for(let j=0;j<len;j++){const a=candidateSegs[j]??0;const b=bestSegs[j]??0;if(a>b){candidateWins=true;break}if(a<b)break}if(candidateWins){best=candidate;bestSegs=candidateSegs}}return best}export{ModuleManager};
@@ -14,4 +14,4 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import{ComponentBase}from"#factories/component-base";import{resolveWrapper}from"#handlers/unified-wrapper";class OwnershipManager extends ComponentBase{static slothletProperty="ownership";constructor(slothlet){super(slothlet);this.moduleToPath=new Map;this.pathToModule=new Map;this._unregisteredModules=new Set;this.moduleEndpoints=new Map}setModuleEndpoint(moduleID,endpoint){if(typeof moduleID==="string"&&moduleID){this.moduleEndpoints.set(moduleID,endpoint)}}getModuleEndpoint(moduleID){return this.moduleEndpoints.get(moduleID)}register({moduleID,apiPath,value,source="core",collisionMode="error",config=null,filePath=null}){if(!moduleID||typeof moduleID!=="string"){throw new this.SlothletError("OWNERSHIP_INVALID_MODULE_ID",{moduleID},null,{validationError:true})}if(apiPath!==""&&(!apiPath||typeof apiPath!=="string")){throw new this.SlothletError("OWNERSHIP_INVALID_API_PATH",{apiPath},null,{validationError:true})}if(this._unregisteredModules.has(moduleID)){return null}const currentOwner=this.getCurrentOwner(apiPath);if(currentOwner&&currentOwner.moduleID!==moduleID){if(collisionMode==="merge"||collisionMode==="replace"||collisionMode==="merge-replace"){}else if(collisionMode==="skip"){return null}else if(collisionMode==="warn"){if(!config?.silent){new this.SlothletWarning("WARNING_OWNERSHIP_CONFLICT",{apiPath,existingModuleId:currentOwner.moduleID,newModuleId:moduleID})}return null}else{throw new this.SlothletError("OWNERSHIP_CONFLICT",{apiPath,existingModuleId:currentOwner.moduleID,newModuleId:moduleID,validationError:true})}}if(!this.moduleToPath.has(moduleID)){this.moduleToPath.set(moduleID,new Set)}this.moduleToPath.get(moduleID).add(apiPath);if(!this.pathToModule.has(apiPath)){this.pathToModule.set(apiPath,[])}const stack=this.pathToModule.get(apiPath);const existingEntry=stack.find(entry2=>entry2.moduleID===moduleID);if(existingEntry){existingEntry.source=source;existingEntry.timestamp=Date.now();if(value!==void 0){existingEntry.value=value}if(filePath!==null){existingEntry.filePath=filePath}return existingEntry}const entry={moduleID,source,timestamp:Date.now(),value,filePath};this.pathToModule.get(apiPath).push(entry);return entry}unregister(moduleID){const paths=this.moduleToPath.get(moduleID);if(!paths){return{removed:[],rolledBack:[]}}this._unregisteredModules.add(moduleID);const removed=[];const rolledBack=[];for(const apiPath of paths){const result=this.removePath(apiPath,moduleID);if(result.action==="delete"){removed.push(apiPath)}else if(result.action==="restore"){rolledBack.push({apiPath,restoredTo:result.restoreModuleId})}}this.moduleToPath.delete(moduleID);this.moduleEndpoints.delete(moduleID);return{removed,rolledBack}}markUnregistered(moduleID){this._unregisteredModules.add(moduleID);this.moduleEndpoints.delete(moduleID)}removePath(apiPath,moduleID=null){const stack=this.pathToModule.get(apiPath);if(!stack){return{action:"none",removedModuleId:null,restoreModuleId:null}}const index=moduleID?stack.findIndex(entry=>entry.moduleID===moduleID):stack.length-1;if(index===-1){return{action:"none",removedModuleId:null,restoreModuleId:null}}const[removed]=stack.splice(index,1);const removedModuleId=removed.moduleID;if(removedModuleId&&this.moduleToPath.has(removedModuleId)){const pathSet=this.moduleToPath.get(removedModuleId);pathSet.delete(apiPath);if(pathSet.size===0){this.moduleToPath.delete(removedModuleId)}}if(stack.length===0){this.pathToModule.delete(apiPath);return{action:"delete",removedModuleId,restoreModuleId:null}}const previous=stack[stack.length-1];return{action:"restore",removedModuleId,restoreModuleId:previous.moduleID}}getCurrentOwner(apiPath){const stack=this.pathToModule.get(apiPath);if(!stack||stack.length===0)return null;return stack[stack.length-1]}getCurrentValue(apiPath){const owner=this.getCurrentOwner(apiPath);if(!owner)return void 0;const value=owner.value;const rawWrapper=resolveWrapper(value);if(rawWrapper){return rawWrapper.__impl}return value}getModulePaths(moduleID){return Array.from(this.moduleToPath.get(moduleID)||[])}getPathHistory(apiPath){return this.pathToModule.get(apiPath)||[]}ownsPath(moduleID,apiPath){const owner=this.getCurrentOwner(apiPath);return owner&&owner.moduleID===moduleID}getDiagnostics(){return{totalModules:this.moduleToPath.size,totalPaths:this.pathToModule.size,modules:Array.from(this.moduleToPath.entries()).map(([id,paths])=>({moduleID:id,pathCount:paths.size})),conflictedPaths:Array.from(this.pathToModule.entries()).filter(([_,stack])=>stack.length>1).map(([path,stack])=>({apiPath:path,ownerStack:stack.map(e=>e.moduleID)}))}}getPathOwnership(apiPath){const stack=this.pathToModule.get(apiPath);if(!stack||stack.length===0){return null}return new Set(stack.map(entry=>entry.moduleID))}registerSubtree(api,moduleID,path,visited=new WeakSet){if(!api||typeof api!=="object")return;if(visited.has(api)){return}visited.add(api);if(path){this.register({moduleID,apiPath:path,value:api,source:"core",collisionMode:"merge",filePath:null})}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"){this.register({moduleID,apiPath:childPath,value,source:"core",collisionMode:"merge",filePath:null});if(typeof value==="object"&&!Array.isArray(value)){this.registerSubtree(value,moduleID,childPath,visited)}}}}clear(){this.moduleToPath.clear();this.pathToModule.clear();this._unregisteredModules.clear();this.moduleEndpoints.clear()}exportState(){return{moduleToPath:Array.from(this.moduleToPath.entries()).map(([id,paths])=>[id,Array.from(paths)]),pathToModule:Array.from(this.pathToModule.entries())}}importState(state){this.clear();for(const[id,paths]of state.moduleToPath){this.moduleToPath.set(id,new Set(paths))}for(const[path,stack]of state.pathToModule){this.pathToModule.set(path,stack)}}}export{OwnershipManager};
17
+ import{ComponentBase}from"#factories/component-base";import{resolveWrapper}from"#handlers/unified-wrapper";const REGISTRATION_SOURCE_CONFIRM="subtree-confirm";const REGISTRATION_SOURCE_AUTHORITATIVE="core";class OwnershipManager extends ComponentBase{static slothletProperty="ownership";constructor(slothlet){super(slothlet);this.moduleToPath=new Map;this.pathToModule=new Map;this._unregisteredModules=new Set;this.moduleEndpoints=new Map}setModuleEndpoint(moduleID,endpoint){if(typeof moduleID==="string"&&moduleID){this.moduleEndpoints.set(moduleID,endpoint)}}getModuleEndpoint(moduleID){return this.moduleEndpoints.get(moduleID)}register({moduleID,apiPath,value,source="core",collisionMode="error",config=null,filePath=null}){if(!moduleID||typeof moduleID!=="string"){throw new this.SlothletError("OWNERSHIP_INVALID_MODULE_ID",{moduleID},null,{validationError:true})}if(apiPath!==""&&(!apiPath||typeof apiPath!=="string")){throw new this.SlothletError("OWNERSHIP_INVALID_API_PATH",{apiPath},null,{validationError:true})}if(this._unregisteredModules.has(moduleID)){return null}const currentOwner=this.getCurrentOwner(apiPath);if(currentOwner&&currentOwner.moduleID!==moduleID){if(collisionMode==="merge"||collisionMode==="replace"||collisionMode==="merge-replace"){}else if(collisionMode==="skip"){return null}else if(collisionMode==="warn"){if(!config?.silent){new this.SlothletWarning("WARNING_OWNERSHIP_CONFLICT",{apiPath,existingModuleId:currentOwner.moduleID,newModuleId:moduleID})}return null}else{throw new this.SlothletError("OWNERSHIP_CONFLICT",{apiPath,existingModuleId:currentOwner.moduleID,newModuleId:moduleID,validationError:true})}}if(!this.moduleToPath.has(moduleID)){this.moduleToPath.set(moduleID,new Set)}this.moduleToPath.get(moduleID).add(apiPath);if(!this.pathToModule.has(apiPath)){this.pathToModule.set(apiPath,[])}const stack=this.pathToModule.get(apiPath);const existingEntry=stack.find(entry2=>entry2.moduleID===moduleID);if(existingEntry){existingEntry.source=source;existingEntry.timestamp=Date.now();if(value!==void 0){existingEntry.value=value}if(filePath!==null){existingEntry.filePath=filePath}if(typeof existingEntry.value==="function"&&source===REGISTRATION_SOURCE_AUTHORITATIVE){if(collisionMode==="replace"||collisionMode==="merge-replace"){existingEntry.isMergeLoss=false;const idx=stack.indexOf(existingEntry);if(idx!==-1&&idx!==stack.length-1){stack.splice(idx,1);stack.push(existingEntry)}}else if(collisionMode==="merge"){existingEntry.isMergeLoss=stack.some(entry2=>entry2!==existingEntry&&!entry2.isMergeLoss&&typeof entry2.value==="function")}}return existingEntry}const isMergeLoss=source!==REGISTRATION_SOURCE_CONFIRM&&Boolean(currentOwner)&&currentOwner.moduleID!==moduleID&&collisionMode==="merge"&&(typeof value==="function"&&typeof currentOwner.value==="function"||typeof currentOwner.value==="function"&&typeof value==="object"&&value!==null);const entry={moduleID,source,timestamp:Date.now(),value,filePath,isMergeLoss};stack.push(entry);return entry}#currentEntry(stack){if(!stack||stack.length===0)return void 0;for(let i=stack.length-1;i>=0;i--){if(!stack[i].isMergeLoss)return stack[i]}return stack[stack.length-1]}unregister(moduleID){const paths=this.moduleToPath.get(moduleID);if(!paths){return{removed:[],rolledBack:[]}}this._unregisteredModules.add(moduleID);const removed=[];const rolledBack=[];for(const apiPath of paths){const result=this.removePath(apiPath,moduleID);if(result.action==="delete"){removed.push(apiPath)}else if(result.action==="restore"){rolledBack.push({apiPath,restoredTo:result.restoreModuleId})}}this.moduleToPath.delete(moduleID);this.moduleEndpoints.delete(moduleID);return{removed,rolledBack}}markUnregistered(moduleID){this._unregisteredModules.add(moduleID);this.moduleEndpoints.delete(moduleID)}clearUnregistered(moduleID){this._unregisteredModules.delete(moduleID)}removePath(apiPath,moduleID=null){const stack=this.pathToModule.get(apiPath);if(!stack){return{action:"none",removedModuleId:null,restoreModuleId:null}}const index=moduleID?stack.findIndex(entry=>entry.moduleID===moduleID):stack.indexOf(this.#currentEntry(stack));if(index===-1){return{action:"none",removedModuleId:null,restoreModuleId:null}}const[removed]=stack.splice(index,1);const removedModuleId=removed.moduleID;if(removedModuleId&&this.moduleToPath.has(removedModuleId)){const pathSet=this.moduleToPath.get(removedModuleId);pathSet.delete(apiPath);if(pathSet.size===0){this.moduleToPath.delete(removedModuleId)}}if(stack.length===0){this.pathToModule.delete(apiPath);return{action:"delete",removedModuleId,restoreModuleId:null}}const previous=this.#currentEntry(stack);return{action:"restore",removedModuleId,restoreModuleId:previous.moduleID}}getCurrentOwner(apiPath){const stack=this.pathToModule.get(apiPath);if(!stack||stack.length===0)return null;return this.#currentEntry(stack)??null}getCurrentValue(apiPath){const owner=this.getCurrentOwner(apiPath);if(!owner)return void 0;const value=owner.value;const rawWrapper=resolveWrapper(value);if(rawWrapper){return rawWrapper.__impl}return value}getModulePaths(moduleID){return Array.from(this.moduleToPath.get(moduleID)||[])}getPathHistory(apiPath){return this.pathToModule.get(apiPath)||[]}ownsPath(moduleID,apiPath){const owner=this.getCurrentOwner(apiPath);return owner&&owner.moduleID===moduleID}getDiagnostics(){return{totalModules:this.moduleToPath.size,totalPaths:this.pathToModule.size,modules:Array.from(this.moduleToPath.entries()).map(([id,paths])=>({moduleID:id,pathCount:paths.size})),conflictedPaths:Array.from(this.pathToModule.entries()).filter(([_,stack])=>stack.length>1).map(([path,stack])=>({apiPath:path,ownerStack:stack.map(e=>e.moduleID)}))}}getPathOwnership(apiPath){const stack=this.pathToModule.get(apiPath);if(!stack||stack.length===0){return null}return new Set(stack.map(entry=>entry.moduleID))}registerSubtree(api,moduleID,path,visited=new WeakSet){if(!api||typeof api!=="object")return;if(visited.has(api)){return}visited.add(api);if(path){this.register({moduleID,apiPath:path,value:api,source:REGISTRATION_SOURCE_CONFIRM,collisionMode:"merge",filePath:null})}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"){this.register({moduleID,apiPath:childPath,value,source:REGISTRATION_SOURCE_CONFIRM,collisionMode:"merge",filePath:null});if(typeof value==="object"&&!Array.isArray(value)){this.registerSubtree(value,moduleID,childPath,visited)}}}}snapshotModuleEntries(moduleID){const snapshot=new Map;for(const path of this.moduleToPath.get(moduleID)||[]){const entry=this.pathToModule.get(path)?.find(candidate=>candidate.moduleID===moduleID);if(!entry)continue;snapshot.set(path,{value:entry.value,filePath:entry.filePath,source:entry.source,isMergeLoss:entry.isMergeLoss})}return snapshot}restoreEntry(moduleID,apiPath,snapshot){const entry=this.pathToModule.get(apiPath)?.find(candidate=>candidate.moduleID===moduleID);if(!entry)return;entry.value=snapshot.value;entry.filePath=snapshot.filePath;entry.source=snapshot.source;entry.isMergeLoss=snapshot.isMergeLoss}snapshotPathEntry(apiPath,moduleID){const entry=this.pathToModule.get(apiPath)?.find(candidate=>candidate.moduleID===moduleID);if(!entry)return void 0;return{value:entry.value,filePath:entry.filePath,source:entry.source,isMergeLoss:entry.isMergeLoss}}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){this.restoreEntry(moduleID,revertPath,prior)}else{this.removePath(revertPath,moduleID)}};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){for(const path of[...this.moduleToPath.get(moduleID)||[]]){const prior=priorEntries.get(path);if(prior){this.restoreEntry(moduleID,path,prior)}else{this.removePath(path,moduleID)}}}clear(){this.moduleToPath.clear();this.pathToModule.clear();this._unregisteredModules.clear();this.moduleEndpoints.clear()}exportState(){return{moduleToPath:Array.from(this.moduleToPath.entries()).map(([id,paths])=>[id,Array.from(paths)]),pathToModule:Array.from(this.pathToModule.entries())}}importState(state){this.clear();for(const[id,paths]of state.moduleToPath){this.moduleToPath.set(id,new Set(paths))}for(const[path,stack]of state.pathToModule){this.pathToModule.set(path,stack)}}}export{OwnershipManager};
@@ -0,0 +1,17 @@
1
+ /*
2
+ Copyright 2026 CLDMV/Shinrai
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
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};